├── .gitignore ├── README.md ├── go-demo ├── .env.example ├── LICENSE ├── Makefile ├── README.md ├── cmd │ └── go-bot-demo │ │ ├── keys.go │ │ ├── main.go │ │ ├── metrics.go │ │ ├── options.go │ │ ├── trading.go │ │ └── util.go ├── go-bot-demo.sh ├── go.mod ├── go.sum ├── metrics │ ├── client.go │ └── metrics.go ├── restart.sh ├── trading │ ├── common.go │ ├── common_derivatives.go │ ├── inj_balances.go │ ├── inj_orders_engine.go │ ├── inj_position.go │ ├── inj_stream.go │ ├── mm_strategy.go │ ├── service.go │ ├── singleExchangeMM.go │ └── singleExchangeMMparts.go └── version │ └── version.go ├── logos └── Logo_stacked_Brand_Black_with_space.png └── python-demo ├── README.md ├── core ├── object.py └── templates │ ├── market_making_template.py │ ├── perp_template.py │ └── spot_template.py ├── requirements.txt ├── strategy ├── advanced_market_making │ ├── .images │ │ ├── Openorders.png │ │ └── orderbook.png │ ├── README.md │ ├── avellaneda_stoikov.py │ ├── configs.ini │ ├── market_making.py │ └── start.py ├── cross_exchange_market_making │ ├── README.md │ ├── configs.ini │ ├── cross_exchange_market_making_batch.py │ ├── start.py │ └── test.py ├── funding_arbitrage │ ├── README.md │ ├── configs.ini │ ├── funding_arbitrage.py │ └── start.py ├── mean_reversion │ ├── README.md │ ├── configs.ini │ ├── mean_reversion_strategy.py │ └── start.py ├── pure_arbitrage │ ├── README.md │ ├── configs.ini │ ├── perp_arbitrage.py │ └── start.py ├── pure_perp_market_making │ ├── README.md │ ├── configs.ini │ ├── perp_simple_strategy.py │ └── start.py └── pure_spot_market_making │ ├── README.md │ ├── configs.ini │ ├── spot_simple_strategy.py │ └── start.py └── util ├── data_manager.py ├── decimal_utils.py └── misc.py /.gitignore: -------------------------------------------------------------------------------- 1 | *my-configs.ini 2 | */log 3 | *.json 4 | # injective folder 5 | */var/data/injective-888/ 6 | 7 | .DS_Store 8 | 9 | # Byte-compiled / optimized / DLL files 10 | __pycache__/ 11 | *.py[cod] 12 | *$py.class 13 | 14 | # C extensions 15 | *.so 16 | 17 | 18 | # Distribution / packaging 19 | .Python 20 | build/ 21 | develop-eggs/ 22 | dist/ 23 | downloads/ 24 | eggs/ 25 | .eggs/ 26 | lib/ 27 | lib64/ 28 | parts/ 29 | sdist/ 30 | var/ 31 | wheels/ 32 | share/python-wheels/ 33 | *.egg-info/ 34 | .installed.cfg 35 | *.egg 36 | MANIFEST 37 | 38 | # PyInstaller 39 | # Usually these files are written by a python script from a template 40 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 41 | *.manifest 42 | *.spec 43 | 44 | # Installer logs 45 | pip-log.txt 46 | pip-delete-this-directory.txt 47 | 48 | # Unit test / coverage reports 49 | htmlcov/ 50 | .tox/ 51 | .nox/ 52 | .coverage 53 | .coverage.* 54 | .cache 55 | nosetests.xml 56 | coverage.xml 57 | *.cover 58 | *.py,cover 59 | .hypothesis/ 60 | .pytest_cache/ 61 | cover/ 62 | 63 | # Translations 64 | *.mo 65 | *.pot 66 | 67 | # Django stuff: 68 | *.log 69 | local_settings.py 70 | db.sqlite3 71 | db.sqlite3-journal 72 | 73 | # Flask stuff: 74 | instance/ 75 | .webassets-cache 76 | 77 | # Scrapy stuff: 78 | .scrapy 79 | 80 | # Sphinx documentation 81 | docs/_build/ 82 | 83 | # PyBuilder 84 | .pybuilder/ 85 | target/ 86 | 87 | # Jupyter Notebook 88 | .ipynb_checkpoints 89 | 90 | # IPython 91 | profile_default/ 92 | ipython_config.py 93 | 94 | # pyenv 95 | # For a library or package, you might want to ignore these files since the code is 96 | # intended to run in multiple environments; otherwise, check them in: 97 | # .python-version 98 | 99 | # pipenv 100 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 101 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 102 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 103 | # install all needed dependencies. 104 | #Pipfile.lock 105 | 106 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 107 | __pypackages__/ 108 | 109 | # Celery stuff 110 | celerybeat-schedule 111 | celerybeat.pid 112 | 113 | # SageMath parsed files 114 | *.sage.py 115 | 116 | # Environments 117 | .env 118 | .venv 119 | env/ 120 | venv/ 121 | ENV/ 122 | env.bak/ 123 | venv.bak/ 124 | 125 | # Spyder project settings 126 | .spyderproject 127 | .spyproject 128 | 129 | # Rope project settings 130 | .ropeproject 131 | 132 | # mkdocs documentation 133 | /site 134 | 135 | # mypy 136 | .mypy_cache/ 137 | .dmypy.json 138 | dmypy.json 139 | 140 | # Pyre type checker 141 | .pyre/ 142 | 143 | # pytype static type analyzer 144 | .pytype/ 145 | 146 | # Cython debug symbols 147 | cython_debug/ 148 | 149 | .idea 150 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # api_demo 2 | ![](./logos/Logo_stacked_Brand_Black_with_space.png) 3 | 4 | Here we include some very practical examples in both Python and GO. 5 | ver 1.0 6 | 7 | #### Disclaimer 8 | This repository and the information contained herein (collectively, this "Repository") has been provided for informational and discussion purposes only, and does not constitute nor shall it be construed as offering legal, financial, investment, or business advice. No warranties or guarantees are made about the accuracy of any information or code contained herein. The legal and regulatory risks inherent to digital assets are not the subject of this Repository, and no decision to buy, sell, exchange, or otherwise utilize any digital asset is recommended based on the content of this Repository. USE AT YOUR OWN RISK. THE AUTHORS AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR TRADING RESULTS. For guidance regarding the possibility of said risks, one should consult with his or her own appropriate legal and/or regulatory counsel. One should consult one’s own advisors for any and all matters related to finance, business, legal, regulatory, and technical knowledge or expertise. Use, fork or publication of this Repository represents an acknowledgment that Anti Social Social Capital Ltd and Injective Labs Inc. are indemnified from any form of liability associated with actions taken by any other interested party. That explicit indemnification is acknowledged by the user or publisher to be legally binding and severable from all other portions of this document. 9 | -------------------------------------------------------------------------------- /go-demo/.env.example: -------------------------------------------------------------------------------- 1 | TRADING_ENV="local" 2 | TRADING_LOG_LEVEL="info" 3 | TRADING_SERVICE_WAIT_TIMEOUT="1m" 4 | 5 | TRADING_COSMOS_CHAIN_ID="injective-1" 6 | TRADING_COSMOS_GRPC="tcp://sentry0.injective.network:9900" 7 | TRADING_TENDERMINT_RPC="http://sentry0.injective.network:26657" 8 | TRADING_COSMOS_GAS_PRICES="500000000inj" 9 | 10 | TRADING_COSMOS_KEYRING="file" 11 | TRADING_COSMOS_KEYRING_DIR= 12 | TRADING_COSMOS_KEYRING_APP="injectived" 13 | TRADING_COSMOS_FROM= 14 | TRADING_COSMOS_FROM_PASSPHRASE= 15 | TRADING_COSMOS_PK="" 16 | TRADING_COSMOS_USE_LEDGER=false 17 | 18 | TRADING_EXCHANGE_GRPC="tcp://sentry0.injective.network:9910" 19 | 20 | TRADING_STATSD_PREFIX="trading." 21 | TRADING_STATSD_ADDR="localhost:8125" 22 | TRADING_STATSD_STUCK_DUR="5m" 23 | TRADING_STATSD_MOCKING=false 24 | TRADING_STATSD_DISABLED=false 25 | 26 | INJECTIVE_SYMBOLS="BTC/USDT" 27 | MAX_ORDER_VALUE_USD=100 -------------------------------------------------------------------------------- /go-demo/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /go-demo/Makefile: -------------------------------------------------------------------------------- 1 | APP_VERSION = $(shell git describe --abbrev=0 --tags) 2 | GIT_COMMIT = $(shell git rev-parse --short HEAD) 3 | BUILD_DATE = $(shell date -u "+%Y%m%d-%H%M") 4 | VERSION_PKG = go-bot-demo/version 5 | IMAGE_NAME := go-bot-demo 6 | 7 | all: 8 | 9 | image: 10 | docker build --build-arg GIT_COMMIT=$(GIT_COMMIT) -t $(IMAGE_NAME):local -f Dockerfile . 11 | docker tag $(IMAGE_NAME):local $(IMAGE_NAME):$(GIT_COMMIT) 12 | docker tag $(IMAGE_NAME):local $(IMAGE_NAME):latest 13 | 14 | push: 15 | docker push $(IMAGE_NAME):$(GIT_COMMIT) 16 | docker push $(IMAGE_NAME):latest 17 | 18 | install: export GOPROXY=direct 19 | install: export VERSION_FLAGS="-X $(VERSION_PKG).GitCommit=$(GIT_COMMIT) -X $(VERSION_PKG).BuildDate=$(BUILD_DATE)" 20 | install: 21 | go install \ 22 | -ldflags $(VERSION_FLAGS) \ 23 | ./cmd/... 24 | 25 | .PHONY: install image push test gen 26 | 27 | test: 28 | # go clean -testcache 29 | go test ./test/... 30 | -------------------------------------------------------------------------------- /go-demo/README.md: -------------------------------------------------------------------------------- 1 | # injective-api-demo-go 2 | 3 | | Author | Email | 4 | |------------|---------------------------| 5 | |Po Wen Perng|powen@injectiveprotocol.com| 6 | 7 | ## Prerequisite 8 | go 1.16+ 9 | 10 | ## How to run demo 11 | This demo is a single exchange market making bot by using go-sdk. 12 | 13 | The simple demo of go-sdk you can check [here](https://github.com/InjectiveLabs/injective-api-demo/tree/go_sdk_demo). 14 | 15 | To set up the environment, you can check file **.env.example**. 16 | 17 | Once setting up the environment, change the file name from *.env.example* to **.env** 18 | 19 | Also, you will need private key from your wallet to fill up TRADING_COSMOS_PK in **.env** file 20 | 21 | Then 22 | 23 | ```bash 24 | $ cd /path/to/injective-api-demo/go 25 | $ make install 26 | $ ./go-bot-demo.sh 27 | ``` 28 | 29 | ## How it works 30 | This market making bot based on [Avellaneda & Stoikov’s market-making strategy](https://hummingbot.io/blog/2021-04-avellaneda-stoikov-market-making-strategy). 31 | 32 | It will place orders on both buy and sell side based on your inventory conditions. 33 | 34 | The reference price is from *Binance* partial book data. 35 | 36 | You can find out all the detail from /path/to/injective-api-demo/go/trading. 37 | 38 | The main loop logic is in singleExchangeMM.go 39 | 40 | The strategy logic is in mm_strategy.go 41 | 42 | Orders managing logic is in inj_orders_engine.go 43 | 44 | Injective stream data handling logic is in inj_stream.go 45 | 46 | Injective stream position handling logic is in inj_position.go 47 | 48 | Feel free to do adjustments to fit your own needs. 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /go-demo/cmd/go-bot-demo/keys.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "io" 7 | "log" 8 | "os" 9 | "path/filepath" 10 | 11 | cosmcrypto "github.com/cosmos/cosmos-sdk/crypto" 12 | "github.com/cosmos/cosmos-sdk/crypto/keyring" 13 | cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" 14 | cosmtypes "github.com/cosmos/cosmos-sdk/types" 15 | "github.com/pkg/errors" 16 | 17 | "github.com/InjectiveLabs/sdk-go/chain/crypto/ethsecp256k1" 18 | "github.com/InjectiveLabs/sdk-go/chain/crypto/hd" 19 | ) 20 | 21 | const defaultKeyringKeyName = "validator" 22 | 23 | var emptyCosmosAddress = cosmtypes.AccAddress{} 24 | 25 | func initCosmosKeyring( 26 | cosmosKeyringDir *string, 27 | cosmosKeyringAppName *string, 28 | cosmosKeyringBackend *string, 29 | cosmosKeyFrom *string, 30 | cosmosKeyPassphrase *string, 31 | cosmosPrivKey *string, 32 | cosmosUseLedger *bool, 33 | ) (cosmtypes.AccAddress, keyring.Keyring, error) { 34 | switch { 35 | case len(*cosmosPrivKey) > 0: 36 | if *cosmosUseLedger { 37 | err := errors.New("cannot combine ledger and privkey options") 38 | return emptyCosmosAddress, nil, err 39 | } 40 | 41 | pkBytes, err := hexToBytes(*cosmosPrivKey) 42 | if err != nil { 43 | err = errors.Wrap(err, "failed to hex-decode cosmos account privkey") 44 | return emptyCosmosAddress, nil, err 45 | } 46 | 47 | // Specfic to Injective chain with Ethermint keys 48 | // Should be secp256k1.PrivKey for generic Cosmos chain 49 | cosmosAccPk := ðsecp256k1.PrivKey{ 50 | Key: pkBytes, 51 | } 52 | 53 | addressFromPk := cosmtypes.AccAddress(cosmosAccPk.PubKey().Address().Bytes()) 54 | 55 | var keyName string 56 | 57 | // check that if cosmos 'From' specified separately, it must match the provided privkey, 58 | if len(*cosmosKeyFrom) > 0 { 59 | addressFrom, err := cosmtypes.AccAddressFromBech32(*cosmosKeyFrom) 60 | if err == nil { 61 | if !bytes.Equal(addressFrom.Bytes(), addressFromPk.Bytes()) { 62 | err = errors.Errorf("expected account address %s but got %s from the private key", addressFrom.String(), addressFromPk.String()) 63 | return emptyCosmosAddress, nil, err 64 | } 65 | } else { 66 | // use it as a name then 67 | keyName = *cosmosKeyFrom 68 | } 69 | } 70 | 71 | if len(keyName) == 0 { 72 | keyName = defaultKeyringKeyName 73 | } 74 | 75 | // wrap a PK into a Keyring 76 | kb, err := KeyringForPrivKey(keyName, cosmosAccPk) 77 | return addressFromPk, kb, err 78 | 79 | case len(*cosmosKeyFrom) > 0: 80 | var fromIsAddress bool 81 | addressFrom, err := cosmtypes.AccAddressFromBech32(*cosmosKeyFrom) 82 | if err == nil { 83 | fromIsAddress = true 84 | } 85 | 86 | var passReader io.Reader = os.Stdin 87 | if len(*cosmosKeyPassphrase) > 0 { 88 | passReader = newPassReader(*cosmosKeyPassphrase) 89 | } 90 | 91 | var absoluteKeyringDir string 92 | if filepath.IsAbs(*cosmosKeyringDir) { 93 | absoluteKeyringDir = *cosmosKeyringDir 94 | } else { 95 | absoluteKeyringDir, _ = filepath.Abs(*cosmosKeyringDir) 96 | } 97 | 98 | kb, err := keyring.New( 99 | *cosmosKeyringAppName, 100 | *cosmosKeyringBackend, 101 | absoluteKeyringDir, 102 | passReader, 103 | hd.EthSecp256k1Option(), 104 | ) 105 | if err != nil { 106 | err = errors.Wrap(err, "failed to init keyring") 107 | return emptyCosmosAddress, nil, err 108 | } 109 | 110 | var keyInfo keyring.Info 111 | if fromIsAddress { 112 | if keyInfo, err = kb.KeyByAddress(addressFrom); err != nil { 113 | err = errors.Wrapf(err, "couldn't find an entry for the key %s in keybase", addressFrom.String()) 114 | return emptyCosmosAddress, nil, err 115 | } 116 | } else { 117 | if keyInfo, err = kb.Key(*cosmosKeyFrom); err != nil { 118 | err = errors.Wrapf(err, "could not find an entry for the key '%s' in keybase", *cosmosKeyFrom) 119 | return emptyCosmosAddress, nil, err 120 | } 121 | } 122 | 123 | switch keyType := keyInfo.GetType(); keyType { 124 | case keyring.TypeLocal: 125 | // kb has a key and it's totally usable 126 | return keyInfo.GetAddress(), kb, nil 127 | case keyring.TypeLedger: 128 | // the kb stores references to ledger keys, so we must explicitly 129 | // check that. kb doesn't know how to scan HD keys - they must be added manually before 130 | if *cosmosUseLedger { 131 | return keyInfo.GetAddress(), kb, nil 132 | } 133 | err := errors.Errorf("'%s' key is a ledger reference, enable ledger option", keyInfo.GetName()) 134 | return emptyCosmosAddress, nil, err 135 | case keyring.TypeOffline: 136 | err := errors.Errorf("'%s' key is an offline key, not supported yet", keyInfo.GetName()) 137 | return emptyCosmosAddress, nil, err 138 | case keyring.TypeMulti: 139 | err := errors.Errorf("'%s' key is an multisig key, not supported yet", keyInfo.GetName()) 140 | return emptyCosmosAddress, nil, err 141 | default: 142 | err := errors.Errorf("'%s' key has unsupported type: %s", keyInfo.GetName(), keyType) 143 | return emptyCosmosAddress, nil, err 144 | } 145 | 146 | default: 147 | err := errors.New("insufficient cosmos key details provided") 148 | return emptyCosmosAddress, nil, err 149 | } 150 | } 151 | 152 | func newPassReader(pass string) io.Reader { 153 | return &passReader{ 154 | pass: pass, 155 | buf: new(bytes.Buffer), 156 | } 157 | } 158 | 159 | type passReader struct { 160 | pass string 161 | buf *bytes.Buffer 162 | } 163 | 164 | var _ io.Reader = &passReader{} 165 | 166 | func (r *passReader) Read(p []byte) (n int, err error) { 167 | n, err = r.buf.Read(p) 168 | if err == io.EOF || n == 0 { 169 | r.buf.WriteString(r.pass + "\n") 170 | 171 | n, err = r.buf.Read(p) 172 | } 173 | 174 | return 175 | } 176 | 177 | // KeyringForPrivKey creates a temporary in-mem keyring for a PrivKey. 178 | // Allows to init Context when the key has been provided in plaintext and parsed. 179 | func KeyringForPrivKey(name string, privKey cryptotypes.PrivKey) (keyring.Keyring, error) { 180 | kb := keyring.NewInMemory(hd.EthSecp256k1Option()) 181 | tmpPhrase := randPhrase(64) 182 | armored := cosmcrypto.EncryptArmorPrivKey(privKey, tmpPhrase, privKey.Type()) 183 | err := kb.ImportPrivKey(name, armored, tmpPhrase) 184 | if err != nil { 185 | err = errors.Wrap(err, "failed to import privkey") 186 | return nil, err 187 | } 188 | 189 | return kb, nil 190 | } 191 | 192 | func randPhrase(size int) string { 193 | buf := make([]byte, size) 194 | _, err := rand.Read(buf) 195 | orPanic(err) 196 | 197 | return string(buf) 198 | } 199 | 200 | func orPanic(err error) { 201 | if err != nil { 202 | log.Panicln() 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /go-demo/cmd/go-bot-demo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | cli "github.com/jawher/mow.cli" 8 | log "github.com/xlab/suplog" 9 | 10 | "go-bot-demo/version" 11 | ) 12 | 13 | var app = cli.App("injective-trading-bot", "Injective's Liquidator Bot.") 14 | 15 | var ( 16 | envName *string 17 | appLogLevel *string 18 | svcWaitTimeout *string 19 | ) 20 | 21 | func main() { 22 | readEnv() 23 | initGlobalOptions( 24 | &envName, 25 | &appLogLevel, 26 | &svcWaitTimeout, 27 | ) 28 | 29 | app.Before = func() { 30 | log.DefaultLogger.SetLevel(logLevel(*appLogLevel)) 31 | } 32 | 33 | app.Command("start", "Starts the trading main loop.", tradingCmd) 34 | app.Command("version", "Print the version information and exit.", versionCmd) 35 | 36 | _ = app.Run(os.Args) 37 | } 38 | 39 | func versionCmd(c *cli.Cmd) { 40 | c.Action = func() { 41 | fmt.Println(version.Version()) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /go-demo/cmd/go-bot-demo/metrics.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "github.com/xlab/closer" 8 | log "github.com/xlab/suplog" 9 | 10 | // DEBUG: do not enable in production 11 | // _ "net/http/pprof" 12 | 13 | "go-bot-demo/metrics" 14 | ) 15 | 16 | // startMetricsGathering initializes metric reporting client, 17 | // if not globally disabled by the config. 18 | func startMetricsGathering( 19 | statsdPrefix *string, 20 | statsdAddr *string, 21 | statsdStuckDur *string, 22 | statsdMocking *string, 23 | statsdDisabled *string, 24 | ) { 25 | if toBool(*statsdDisabled) { 26 | // initializes statsd client with a mock one with no-op enabled 27 | metrics.Disable() 28 | return 29 | } 30 | 31 | go func() { 32 | for { 33 | hostname, _ := os.Hostname() 34 | err := metrics.Init(*statsdAddr, checkStatsdPrefix(*statsdPrefix), &metrics.StatterConfig{ 35 | EnvName: *envName, 36 | HostName: hostname, 37 | StuckFunctionTimeout: duration(*statsdStuckDur, 30*time.Minute), 38 | MockingEnabled: toBool(*statsdMocking) || *envName == "local", 39 | }) 40 | if err != nil { 41 | log.WithError(err).Warningln("metrics init failed, will retry in 1 min") 42 | time.Sleep(time.Minute) 43 | continue 44 | } 45 | break 46 | } 47 | 48 | closer.Bind(func() { 49 | metrics.Close() 50 | }) 51 | }() 52 | 53 | } 54 | -------------------------------------------------------------------------------- /go-demo/cmd/go-bot-demo/options.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import cli "github.com/jawher/mow.cli" 4 | 5 | // initGlobalOptions defines some global CLI options, that are useful for most parts of the app. 6 | // Before adding option to there, consider moving it into the actual Cmd. 7 | func initGlobalOptions( 8 | envName **string, 9 | appLogLevel **string, 10 | svcWaitTimeout **string, 11 | ) { 12 | *envName = app.String(cli.StringOpt{ 13 | Name: "e env", 14 | Desc: "The environment name this app runs in. Used for metrics and error reporting.", 15 | EnvVar: "TRADING_ENV", 16 | Value: "local", 17 | }) 18 | 19 | *appLogLevel = app.String(cli.StringOpt{ 20 | Name: "l log-level", 21 | Desc: "Available levels: error, warn, info, debug.", 22 | EnvVar: "TRADING_LOG_LEVEL", 23 | Value: "info", 24 | }) 25 | 26 | *svcWaitTimeout = app.String(cli.StringOpt{ 27 | Name: "svc-wait-timeout", 28 | Desc: "Standard wait timeout for external services (e.g. Cosmos daemon GRPC connection)", 29 | EnvVar: "TRADING_SERVICE_WAIT_TIMEOUT", 30 | Value: "1m", 31 | }) 32 | } 33 | 34 | func initCosmosOptions( 35 | cmd *cli.Cmd, 36 | cosmosChainID **string, 37 | cosmosGRPC **string, 38 | tendermintRPC **string, 39 | cosmosGasPrices **string, 40 | ) { 41 | *cosmosChainID = cmd.String(cli.StringOpt{ 42 | Name: "cosmos-chain-id", 43 | Desc: "Specify Chain ID of the Cosmos network.", 44 | EnvVar: "TRADING_COSMOS_CHAIN_ID", 45 | Value: "injective-1", 46 | }) 47 | 48 | *cosmosGRPC = cmd.String(cli.StringOpt{ 49 | Name: "cosmos-grpc", 50 | Desc: "Cosmos GRPC querying endpoint", 51 | EnvVar: "TRADING_COSMOS_GRPC", 52 | Value: "tcp://localhost:9900", 53 | }) 54 | 55 | *tendermintRPC = cmd.String(cli.StringOpt{ 56 | Name: "tendermint-rpc", 57 | Desc: "Tendermint RPC endpoint", 58 | EnvVar: "TRADING_TENDERMINT_RPC", 59 | Value: "http://localhost:26657", 60 | }) 61 | 62 | *cosmosGasPrices = cmd.String(cli.StringOpt{ 63 | Name: "cosmos-gas-prices", 64 | Desc: "Specify Cosmos chain transaction fees as sdk.Coins gas prices", 65 | EnvVar: "TRADING_COSMOS_GAS_PRICES", 66 | Value: "", // example: 500000000inj 67 | }) 68 | } 69 | 70 | func initCosmosKeyOptions( 71 | cmd *cli.Cmd, 72 | cosmosKeyringDir **string, 73 | cosmosKeyringAppName **string, 74 | cosmosKeyringBackend **string, 75 | cosmosKeyFrom **string, 76 | cosmosKeyPassphrase **string, 77 | cosmosPrivKey **string, 78 | cosmosUseLedger **bool, 79 | ) { 80 | *cosmosKeyringBackend = cmd.String(cli.StringOpt{ 81 | Name: "cosmos-keyring", 82 | Desc: "Specify Cosmos keyring backend (os|file|kwallet|pass|test)", 83 | EnvVar: "TRADING_COSMOS_KEYRING", 84 | Value: "file", 85 | }) 86 | 87 | *cosmosKeyringDir = cmd.String(cli.StringOpt{ 88 | Name: "cosmos-keyring-dir", 89 | Desc: "Specify Cosmos keyring dir, if using file keyring.", 90 | EnvVar: "TRADING_COSMOS_KEYRING_DIR", 91 | Value: "", 92 | }) 93 | 94 | *cosmosKeyringAppName = cmd.String(cli.StringOpt{ 95 | Name: "cosmos-keyring-app", 96 | Desc: "Specify Cosmos keyring app name.", 97 | EnvVar: "TRADING_COSMOS_KEYRING_APP", 98 | Value: "injectived", 99 | }) 100 | 101 | *cosmosKeyFrom = cmd.String(cli.StringOpt{ 102 | Name: "cosmos-from", 103 | Desc: "Specify the Cosmos validator key name or address. If specified, must exist in keyring, ledger or match the privkey.", 104 | EnvVar: "TRADING_COSMOS_FROM", 105 | }) 106 | 107 | *cosmosKeyPassphrase = cmd.String(cli.StringOpt{ 108 | Name: "cosmos-from-passphrase", 109 | Desc: "Specify keyring passphrase, otherwise Stdin will be used.", 110 | EnvVar: "TRADING_COSMOS_FROM_PASSPHRASE", 111 | }) 112 | 113 | *cosmosPrivKey = cmd.String(cli.StringOpt{ 114 | Name: "cosmos-pk", 115 | Desc: "Provide a raw Cosmos account private key of the validator in hex. USE FOR TESTING ONLY!", 116 | EnvVar: "TRADING_COSMOS_PK", 117 | }) 118 | 119 | *cosmosUseLedger = cmd.Bool(cli.BoolOpt{ 120 | Name: "cosmos-use-ledger", 121 | Desc: "Use the Cosmos app on hardware ledger to sign transactions.", 122 | EnvVar: "TRADING_COSMOS_USE_LEDGER", 123 | Value: false, 124 | }) 125 | } 126 | 127 | func initExchangeOptions( 128 | cmd *cli.Cmd, 129 | exchangeGRPC **string, 130 | ) { 131 | *exchangeGRPC = cmd.String(cli.StringOpt{ 132 | Name: "exchange-grpc", 133 | Desc: "Exchange API (GRPC) endpoint", 134 | EnvVar: "TRADING_EXCHANGE_GRPC", 135 | Value: "tcp://localhost:9910", 136 | }) 137 | } 138 | 139 | // initStatsdOptions sets options for StatsD metrics. 140 | func initStatsdOptions( 141 | cmd *cli.Cmd, 142 | statsdPrefix **string, 143 | statsdAddr **string, 144 | statsdStuckDur **string, 145 | statsdMocking **string, 146 | statsdDisabled **string, 147 | ) { 148 | *statsdPrefix = cmd.String(cli.StringOpt{ 149 | Name: "statsd-prefix", 150 | Desc: "Specify StatsD compatible metrics prefix.", 151 | EnvVar: "TRADING_STATSD_PREFIX", 152 | Value: "trading", 153 | }) 154 | 155 | *statsdAddr = cmd.String(cli.StringOpt{ 156 | Name: "statsd-addr", 157 | Desc: "UDP address of a StatsD compatible metrics aggregator.", 158 | EnvVar: "TRADING_STATSD_ADDR", 159 | Value: "localhost:8125", 160 | }) 161 | 162 | *statsdStuckDur = cmd.String(cli.StringOpt{ 163 | Name: "statsd-stuck-func", 164 | Desc: "Sets a duration to consider a function to be stuck (e.g. in deadlock).", 165 | EnvVar: "TRADING_STATSD_STUCK_DUR", 166 | Value: "5m", 167 | }) 168 | 169 | *statsdMocking = cmd.String(cli.StringOpt{ 170 | Name: "statsd-mocking", 171 | Desc: "If enabled replaces statsd client with a mock one that simply logs values.", 172 | EnvVar: "TRADING_STATSD_MOCKING", 173 | Value: "false", 174 | }) 175 | 176 | *statsdDisabled = cmd.String(cli.StringOpt{ 177 | Name: "statsd-disabled", 178 | Desc: "Force disabling statsd reporting completely.", 179 | EnvVar: "TRADING_STATSD_DISABLED", 180 | Value: "true", 181 | }) 182 | } 183 | 184 | func initTradingOptions( 185 | cmd *cli.Cmd, 186 | injSymbols **[]string, 187 | maxOrderValue **[]float64, 188 | ) { 189 | *injSymbols = cmd.Strings(cli.StringsOpt{ 190 | Name: "injective-symbols", 191 | Desc: "injective market for strategy", 192 | EnvVar: "INJECTIVE_SYMBOLS", 193 | Value: []string{}, 194 | }) 195 | 196 | *maxOrderValue = cmd.Floats64(cli.Floats64Opt{ 197 | Name: "max-order-value", 198 | Desc: "max order value in usd", 199 | EnvVar: "MAX_ORDER_VALUE_USD", 200 | Value: []float64{}, 201 | }) 202 | } 203 | -------------------------------------------------------------------------------- /go-demo/cmd/go-bot-demo/trading.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "time" 7 | 8 | banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" 9 | 10 | cli "github.com/jawher/mow.cli" 11 | rpchttp "github.com/tendermint/tendermint/rpc/client/http" 12 | "github.com/xlab/closer" 13 | log "github.com/xlab/suplog" 14 | 15 | "go-bot-demo/trading" 16 | 17 | chainclient "github.com/InjectiveLabs/sdk-go/chain/client" 18 | chaintypes "github.com/InjectiveLabs/sdk-go/chain/exchange/types" 19 | accountsPB "github.com/InjectiveLabs/sdk-go/exchange/accounts_rpc/pb" 20 | derivativeExchangePB "github.com/InjectiveLabs/sdk-go/exchange/derivative_exchange_rpc/pb" 21 | exchangePB "github.com/InjectiveLabs/sdk-go/exchange/exchange_rpc/pb" 22 | oraclePB "github.com/InjectiveLabs/sdk-go/exchange/oracle_rpc/pb" 23 | spotExchangePB "github.com/InjectiveLabs/sdk-go/exchange/spot_exchange_rpc/pb" 24 | ) 25 | 26 | // tradingCmd action runs the service 27 | // 28 | // $ injective-trading-bot start 29 | func tradingCmd(cmd *cli.Cmd) { 30 | // orchestrator-specific CLI options 31 | var ( 32 | // Cosmos params 33 | cosmosChainID *string 34 | cosmosGRPC *string 35 | tendermintRPC *string 36 | cosmosGasPrices *string 37 | 38 | // Cosmos Key Management 39 | cosmosKeyringDir *string 40 | cosmosKeyringAppName *string 41 | cosmosKeyringBackend *string 42 | 43 | cosmosKeyFrom *string 44 | cosmosKeyPassphrase *string 45 | cosmosPrivKey *string 46 | cosmosUseLedger *bool 47 | 48 | // Exchange API params 49 | exchangeGRPC *string 50 | 51 | // Metrics 52 | statsdPrefix *string 53 | statsdAddr *string 54 | statsdStuckDur *string 55 | statsdMocking *string 56 | statsdDisabled *string 57 | 58 | // Trading parameters 59 | injSymbols *[]string 60 | maxOrderValue *[]float64 61 | ) 62 | 63 | initCosmosOptions( 64 | cmd, 65 | &cosmosChainID, 66 | &cosmosGRPC, 67 | &tendermintRPC, 68 | &cosmosGasPrices, 69 | ) 70 | 71 | initCosmosKeyOptions( 72 | cmd, 73 | &cosmosKeyringDir, 74 | &cosmosKeyringAppName, 75 | &cosmosKeyringBackend, 76 | &cosmosKeyFrom, 77 | &cosmosKeyPassphrase, 78 | &cosmosPrivKey, 79 | &cosmosUseLedger, 80 | ) 81 | 82 | initExchangeOptions( 83 | cmd, 84 | &exchangeGRPC, 85 | ) 86 | 87 | initStatsdOptions( 88 | cmd, 89 | &statsdPrefix, 90 | &statsdAddr, 91 | &statsdStuckDur, 92 | &statsdMocking, 93 | &statsdDisabled, 94 | ) 95 | 96 | initTradingOptions( 97 | cmd, 98 | &injSymbols, 99 | &maxOrderValue, 100 | ) 101 | 102 | cmd.Action = func() { 103 | // ensure a clean exit 104 | defer closer.Close() 105 | 106 | startMetricsGathering( 107 | statsdPrefix, 108 | statsdAddr, 109 | statsdStuckDur, 110 | statsdMocking, 111 | statsdDisabled, 112 | ) 113 | 114 | if *cosmosUseLedger { 115 | log.Fatalln("cannot really use Ledger for trading service loop, since signatures msut be realtime") 116 | } 117 | 118 | senderAddress, cosmosKeyring, err := initCosmosKeyring( 119 | cosmosKeyringDir, 120 | cosmosKeyringAppName, 121 | cosmosKeyringBackend, 122 | cosmosKeyFrom, 123 | cosmosKeyPassphrase, 124 | cosmosPrivKey, 125 | cosmosUseLedger, 126 | ) 127 | if err != nil { 128 | log.WithError(err).Fatalln("failed to init Cosmos keyring") 129 | } 130 | 131 | log.Infoln("Using Cosmos Sender", senderAddress.String()) 132 | 133 | clientCtx, err := chainclient.NewClientContext(*cosmosChainID, senderAddress.String(), cosmosKeyring) 134 | if err != nil { 135 | log.WithError(err).Fatalln("failed to initialize cosmos client context") 136 | } 137 | clientCtx = clientCtx.WithNodeURI(*tendermintRPC) 138 | tmRPC, err := rpchttp.New(*tendermintRPC, "/websocket") 139 | if err != nil { 140 | log.WithError(err).Fatalln("failed to connect to tendermint RPC") 141 | } 142 | clientCtx = clientCtx.WithClient(tmRPC) 143 | 144 | daemonClient, err := chainclient.NewCosmosClient(clientCtx, *cosmosGRPC, chainclient.OptionGasPrices(*cosmosGasPrices)) 145 | if err != nil { 146 | log.WithError(err).WithFields(log.Fields{ 147 | "endpoint": *cosmosGRPC, 148 | }).Fatalln("failed to connect to daemon, is injectived running?") 149 | } 150 | closer.Bind(func() { 151 | daemonClient.Close() 152 | }) 153 | 154 | log.Infoln("Waiting for GRPC services") 155 | time.Sleep(1 * time.Second) 156 | 157 | daemonWaitCtx, cancelWait := context.WithTimeout(context.Background(), time.Minute) 158 | daemonConn := daemonClient.QueryClient() 159 | waitForService(daemonWaitCtx, daemonConn) 160 | cancelWait() 161 | 162 | exchangeWaitCtx, cancelWait := context.WithTimeout(context.Background(), time.Minute) 163 | exchangeConn, err := grpcDialEndpoint(*exchangeGRPC) 164 | if err != nil { 165 | log.WithError(err).WithFields(log.Fields{ 166 | "endpoint": *exchangeGRPC, 167 | }).Fatalln("failed to connect to API, is injective-exchange running?") 168 | } 169 | waitForService(exchangeWaitCtx, exchangeConn) 170 | cancelWait() 171 | 172 | svc := trading.NewService( 173 | daemonClient, 174 | chaintypes.NewQueryClient(daemonConn), 175 | banktypes.NewQueryClient(daemonConn), 176 | accountsPB.NewInjectiveAccountsRPCClient(exchangeConn), 177 | exchangePB.NewInjectiveExchangeRPCClient(exchangeConn), 178 | spotExchangePB.NewInjectiveSpotExchangeRPCClient(exchangeConn), 179 | derivativeExchangePB.NewInjectiveDerivativeExchangeRPCClient(exchangeConn), 180 | oraclePB.NewInjectiveOracleRPCClient(exchangeConn), 181 | *injSymbols, 182 | *maxOrderValue, 183 | ) 184 | closer.Bind(func() { 185 | svc.Close() 186 | }) 187 | 188 | go func() { 189 | if err := svc.Start(); err != nil { 190 | log.Errorln(err) 191 | 192 | // signal there that the app failed 193 | os.Exit(1) 194 | } 195 | }() 196 | 197 | closer.Hold() 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /go-demo/cmd/go-bot-demo/util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "encoding/hex" 8 | "fmt" 9 | "io/ioutil" 10 | "net" 11 | "os" 12 | "strings" 13 | "time" 14 | 15 | "github.com/pkg/errors" 16 | log "github.com/xlab/suplog" 17 | "google.golang.org/grpc" 18 | "google.golang.org/grpc/connectivity" 19 | ) 20 | 21 | // readEnv is a special utility that reads `.env` file into actual environment variables 22 | // of the current app, similar to `dotenv` Node package. 23 | func readEnv() { 24 | if envdata, _ := ioutil.ReadFile(".env"); len(envdata) > 0 { 25 | s := bufio.NewScanner(bytes.NewReader(envdata)) 26 | for s.Scan() { 27 | parts := strings.Split(s.Text(), "=") 28 | if len(parts) != 2 { 29 | continue 30 | } 31 | strValue := strings.Trim(parts[1], `"`) 32 | if err := os.Setenv(parts[0], strValue); err != nil { 33 | log.WithField("name", parts[0]).WithError(err).Warningln("failed to override ENV variable") 34 | } 35 | } 36 | } 37 | } 38 | 39 | // stdinConfirm checks the user's confirmation, if not forced to Yes 40 | func stdinConfirm(msg string) bool { 41 | var response string 42 | 43 | if _, err := fmt.Scanln(&response); err != nil { 44 | log.WithError(err).Errorln("failed to confirm the action") 45 | return false 46 | } 47 | 48 | switch strings.ToLower(strings.TrimSpace(response)) { 49 | case "y", "yes": 50 | return true 51 | default: 52 | return false 53 | } 54 | } 55 | 56 | // logLevel converts vague log level name into typed level. 57 | func logLevel(s string) log.Level { 58 | switch s { 59 | case "1", "error": 60 | return log.ErrorLevel 61 | case "2", "warn": 62 | return log.WarnLevel 63 | case "3", "info": 64 | return log.InfoLevel 65 | case "4", "debug": 66 | return log.DebugLevel 67 | default: 68 | return log.FatalLevel 69 | } 70 | } 71 | 72 | // toBool is used to parse vague bool definition into typed bool. 73 | func toBool(s string) bool { 74 | switch strings.ToLower(s) { 75 | case "true", "1", "t", "yes": 76 | return true 77 | default: 78 | return false 79 | } 80 | } 81 | 82 | // duration parses duration from string with a provided default fallback. 83 | func duration(s string, defaults time.Duration) time.Duration { 84 | dur, err := time.ParseDuration(s) 85 | if err != nil { 86 | dur = defaults 87 | } 88 | return dur 89 | } 90 | 91 | // checkStatsdPrefix ensures that the statsd prefix really 92 | // have "." at end. 93 | func checkStatsdPrefix(s string) string { 94 | if !strings.HasSuffix(s, ".") { 95 | return s + "." 96 | } 97 | return s 98 | } 99 | 100 | func hexToBytes(str string) ([]byte, error) { 101 | if strings.HasPrefix(str, "0x") { 102 | str = str[2:] 103 | } 104 | 105 | data, err := hex.DecodeString(str) 106 | if err != nil { 107 | return nil, err 108 | } 109 | 110 | return data, nil 111 | } 112 | 113 | func waitForService(ctx context.Context, conn *grpc.ClientConn) error { 114 | for { 115 | select { 116 | case <-ctx.Done(): 117 | return errors.Errorf("Service wait timed out. Please run injective exchange service:\n\nmake install && injective-exchange") 118 | default: 119 | state := conn.GetState() 120 | 121 | if state != connectivity.Ready { 122 | time.Sleep(time.Second) 123 | continue 124 | } 125 | 126 | return nil 127 | } 128 | } 129 | } 130 | 131 | func grpcDialEndpoint(protoAddr string) (conn *grpc.ClientConn, err error) { 132 | conn, err = grpc.Dial(protoAddr, grpc.WithInsecure(), grpc.WithContextDialer(dialerFunc)) 133 | if err != nil { 134 | err := errors.Wrapf(err, "failed to connect to the gRPC: %s", protoAddr) 135 | return nil, err 136 | } 137 | 138 | return conn, nil 139 | } 140 | 141 | // dialerFunc dials the given address and returns a net.Conn. The protoAddr argument should be prefixed with the protocol, 142 | // eg. "tcp://127.0.0.1:8080" or "unix:///tmp/test.sock" 143 | func dialerFunc(ctx context.Context, protoAddr string) (net.Conn, error) { 144 | proto, address := protocolAndAddress(protoAddr) 145 | conn, err := net.Dial(proto, address) 146 | return conn, err 147 | } 148 | 149 | // protocolAndAddress splits an address into the protocol and address components. 150 | // For instance, "tcp://127.0.0.1:8080" will be split into "tcp" and "127.0.0.1:8080". 151 | // If the address has no protocol prefix, the default is "tcp". 152 | func protocolAndAddress(listenAddr string) (string, string) { 153 | protocol, address := "tcp", listenAddr 154 | parts := strings.SplitN(address, "://", 2) 155 | if len(parts) == 2 { 156 | protocol, address = parts[0], parts[1] 157 | } 158 | return protocol, address 159 | } 160 | -------------------------------------------------------------------------------- /go-demo/go-bot-demo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | clear 4 | go-bot-demo start 5 | -------------------------------------------------------------------------------- /go-demo/go.mod: -------------------------------------------------------------------------------- 1 | module go-bot-demo 2 | 3 | go 1.17 4 | 5 | replace github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1 6 | 7 | replace github.com/btcsuite/btcutil => github.com/btcsuite/btcutil v1.0.2 8 | 9 | //replace github.com/cosmos/cosmos-sdk => github.com/InjectiveLabs/cosmos-sdk v0.44.0 10 | 11 | require ( 12 | github.com/InjectiveLabs/sdk-go v1.28.1 13 | github.com/alexcesaro/statsd v2.0.0+incompatible 14 | github.com/cosmos/cosmos-sdk v0.44.3 15 | github.com/ethereum/go-ethereum v1.10.11 16 | github.com/jawher/mow.cli v1.2.0 17 | github.com/pkg/errors v0.9.1 18 | github.com/shopspring/decimal v1.3.1 19 | github.com/sirupsen/logrus v1.8.1 20 | github.com/tendermint/tendermint v0.34.14 21 | github.com/xlab/closer v0.0.0-20190328110542-03326addb7c2 22 | github.com/xlab/suplog v1.3.1 23 | google.golang.org/grpc v1.41.0 24 | ) 25 | 26 | require ( 27 | filippo.io/edwards25519 v1.0.0-beta.2 // indirect 28 | github.com/99designs/keyring v1.1.6 // indirect 29 | github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d // indirect 30 | github.com/DataDog/zstd v1.4.5 // indirect 31 | github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 // indirect 32 | github.com/VictoriaMetrics/fastcache v1.6.0 // indirect 33 | github.com/armon/go-metrics v0.3.9 // indirect 34 | github.com/aws/aws-sdk-go v1.27.0 // indirect 35 | github.com/bandprotocol/bandchain-packet v0.0.2 // indirect 36 | github.com/beorn7/perks v1.0.1 // indirect 37 | github.com/bgentry/speakeasy v0.1.0 // indirect 38 | github.com/btcsuite/btcd v0.22.0-beta // indirect 39 | github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce // indirect 40 | github.com/bugsnag/bugsnag-go v1.5.3 // indirect 41 | github.com/bugsnag/panicwrap v1.2.0 // indirect 42 | github.com/cespare/xxhash v1.1.0 // indirect 43 | github.com/cespare/xxhash/v2 v2.1.1 // indirect 44 | github.com/confio/ics23/go v0.6.6 // indirect 45 | github.com/cosmos/go-bip39 v1.0.0 // indirect 46 | github.com/cosmos/iavl v0.17.1 // indirect 47 | github.com/cosmos/ibc-go/v2 v2.0.0 // indirect 48 | github.com/cosmos/ledger-cosmos-go v0.11.1 // indirect 49 | github.com/cosmos/ledger-go v0.9.2 // indirect 50 | github.com/danieljoos/wincred v1.0.2 // indirect 51 | github.com/davecgh/go-spew v1.1.1 // indirect 52 | github.com/deckarep/golang-set v1.7.1 // indirect 53 | github.com/dgraph-io/badger/v2 v2.2007.2 // indirect 54 | github.com/dgraph-io/ristretto v0.0.3 // indirect 55 | github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect 56 | github.com/dustin/go-humanize v1.0.0 // indirect 57 | github.com/dvsekhvalnov/jose2go v0.0.0-20200901110807-248326c1351b // indirect 58 | github.com/edsrzf/mmap-go v1.0.0 // indirect 59 | github.com/enigmampc/btcutil v1.0.3-0.20200723161021-e2fb6adb2a25 // indirect 60 | github.com/fsnotify/fsnotify v1.5.1 // indirect 61 | github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 // indirect 62 | github.com/go-kit/kit v0.10.0 // indirect 63 | github.com/go-logfmt/logfmt v0.5.0 // indirect 64 | github.com/go-ole/go-ole v1.2.1 // indirect 65 | github.com/go-stack/stack v1.8.0 // indirect 66 | github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect 67 | github.com/gofrs/uuid v3.3.0+incompatible // indirect 68 | github.com/gogo/protobuf v1.3.3 // indirect 69 | github.com/golang/protobuf v1.5.2 // indirect 70 | github.com/golang/snappy v0.0.4 // indirect 71 | github.com/google/btree v1.0.0 // indirect 72 | github.com/google/uuid v1.1.5 // indirect 73 | github.com/gorilla/mux v1.8.0 // indirect 74 | github.com/gorilla/websocket v1.4.2 // indirect 75 | github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect 76 | github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect 77 | github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect 78 | github.com/gtank/merlin v0.1.1 // indirect 79 | github.com/gtank/ristretto255 v0.1.2 // indirect 80 | github.com/hashicorp/go-immutable-radix v1.0.0 // indirect 81 | github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d // indirect 82 | github.com/hashicorp/hcl v1.0.0 // indirect 83 | github.com/hdevalence/ed25519consensus v0.0.0-20210204194344-59a8610d2b87 // indirect 84 | github.com/holiman/bloomfilter/v2 v2.0.3 // indirect 85 | github.com/holiman/uint256 v1.2.0 // indirect 86 | github.com/huin/goupnp v1.0.2 // indirect 87 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 88 | github.com/jackpal/go-nat-pmp v1.0.2 // indirect 89 | github.com/jmespath/go-jmespath v0.4.0 // indirect 90 | github.com/jmhodges/levigo v1.0.0 // indirect 91 | github.com/karalabe/usb v0.0.0-20211005121534-4c5740d64559 // indirect 92 | github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect 93 | github.com/keybase/go-keychain v0.0.0-20190712205309-48d3d31d256d // indirect 94 | github.com/libp2p/go-buffer-pool v0.0.2 // indirect 95 | github.com/magiconair/properties v1.8.5 // indirect 96 | github.com/mattn/go-isatty v0.0.14 // indirect 97 | github.com/mattn/go-runewidth v0.0.9 // indirect 98 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 99 | github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643 // indirect 100 | github.com/mitchellh/go-homedir v1.1.0 // indirect 101 | github.com/mitchellh/mapstructure v1.4.2 // indirect 102 | github.com/mtibben/percent v0.2.1 // indirect 103 | github.com/oklog/ulid v1.3.1 // indirect 104 | github.com/olekukonko/tablewriter v0.0.5 // indirect 105 | github.com/pelletier/go-toml v1.9.4 // indirect 106 | github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 // indirect 107 | github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect 108 | github.com/pmezard/go-difflib v1.0.0 // indirect 109 | github.com/prometheus/client_golang v1.11.0 // indirect 110 | github.com/prometheus/client_model v0.2.0 // indirect 111 | github.com/prometheus/common v0.29.0 // indirect 112 | github.com/prometheus/procfs v0.6.0 // indirect 113 | github.com/prometheus/tsdb v0.10.0 // indirect 114 | github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect 115 | github.com/regen-network/cosmos-proto v0.3.1 // indirect 116 | github.com/rjeczalik/notify v0.9.2 // indirect 117 | github.com/sasha-s/go-deadlock v0.2.1-0.20190427202633-1595213edefa // indirect 118 | github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect 119 | github.com/spf13/afero v1.6.0 // indirect 120 | github.com/spf13/cast v1.4.1 // indirect 121 | github.com/spf13/cobra v1.2.1 // indirect 122 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 123 | github.com/spf13/pflag v1.0.5 // indirect 124 | github.com/spf13/viper v1.8.1 // indirect 125 | github.com/status-im/keycard-go v0.0.0-20200402102358-957c09536969 // indirect 126 | github.com/stretchr/testify v1.7.0 // indirect 127 | github.com/subosito/gotenv v1.2.0 // indirect 128 | github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect 129 | github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c // indirect 130 | github.com/tendermint/btcd v0.1.1 // indirect 131 | github.com/tendermint/crypto v0.0.0-20191022145703-50d29ede1e15 // indirect 132 | github.com/tendermint/go-amino v0.16.0 // indirect 133 | github.com/tendermint/tm-db v0.6.4 // indirect 134 | github.com/tklauser/go-sysconf v0.3.5 // indirect 135 | github.com/tklauser/numcpus v0.2.2 // indirect 136 | github.com/tyler-smith/go-bip39 v1.0.2 // indirect 137 | github.com/zondax/hid v0.9.0 // indirect 138 | go.etcd.io/bbolt v1.3.5 // indirect 139 | golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect 140 | golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f // indirect 141 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect 142 | golang.org/x/sys v0.0.0-20210903071746-97244b99971b // indirect 143 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect 144 | golang.org/x/text v0.3.6 // indirect 145 | google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c // indirect 146 | google.golang.org/protobuf v1.27.1 // indirect 147 | gopkg.in/alexcesaro/statsd.v2 v2.0.0 // indirect 148 | gopkg.in/ini.v1 v1.63.2 // indirect 149 | gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect 150 | gopkg.in/yaml.v2 v2.4.0 // indirect 151 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 152 | ) 153 | -------------------------------------------------------------------------------- /go-demo/metrics/client.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "github.com/alexcesaro/statsd" 8 | "github.com/pkg/errors" 9 | log "github.com/xlab/suplog" 10 | ) 11 | 12 | var client Statter 13 | var clientMux = new(sync.RWMutex) 14 | var config *StatterConfig 15 | 16 | type StatterConfig struct { 17 | EnvName string 18 | HostName string 19 | StuckFunctionTimeout time.Duration 20 | MockingEnabled bool 21 | } 22 | 23 | func (m *StatterConfig) BaseTags() []string { 24 | var baseTags []string 25 | 26 | if len(config.EnvName) > 0 { 27 | baseTags = append(baseTags, "env", config.EnvName) 28 | } 29 | if len(config.HostName) > 0 { 30 | baseTags = append(baseTags, "machine", config.HostName) 31 | } 32 | 33 | return baseTags 34 | } 35 | 36 | type Statter interface { 37 | Count(bucket string, n interface{}) 38 | Increment(bucket string) 39 | Gauge(bucket string, value interface{}) 40 | Timing(bucket string, value interface{}) 41 | Histogram(bucket string, value interface{}) 42 | Unique(bucket string, value string) 43 | Close() 44 | } 45 | 46 | func Close() { 47 | clientMux.RLock() 48 | defer clientMux.RUnlock() 49 | if client == nil { 50 | return 51 | } 52 | client.Close() 53 | } 54 | 55 | func Disable() { 56 | config = checkConfig(nil) 57 | clientMux.Lock() 58 | client = newMockStatter(true) 59 | clientMux.Unlock() 60 | } 61 | 62 | func Init(addr string, prefix string, cfg *StatterConfig) error { 63 | config = checkConfig(cfg) 64 | if config.MockingEnabled { 65 | // init a mock statter instead of real statsd client 66 | clientMux.Lock() 67 | client = newMockStatter(false) 68 | clientMux.Unlock() 69 | return nil 70 | } 71 | statter, err := statsd.New( 72 | statsd.Address(addr), 73 | statsd.Prefix(prefix), 74 | statsd.ErrorHandler(errHandler), 75 | statsd.Tags(config.BaseTags()...), 76 | ) 77 | if err != nil { 78 | err = errors.Wrap(err, "statsd init failed") 79 | return err 80 | } 81 | clientMux.Lock() 82 | client = statter 83 | clientMux.Unlock() 84 | return nil 85 | } 86 | 87 | func checkConfig(cfg *StatterConfig) *StatterConfig { 88 | if cfg == nil { 89 | cfg = &StatterConfig{} 90 | } 91 | if cfg.StuckFunctionTimeout < time.Second { 92 | cfg.StuckFunctionTimeout = 5 * time.Minute 93 | } 94 | if len(cfg.EnvName) == 0 { 95 | cfg.EnvName = "local" 96 | } 97 | return cfg 98 | } 99 | 100 | func errHandler(err error) { 101 | log.WithError(err).Errorln("statsd error") 102 | } 103 | 104 | func newMockStatter(noop bool) Statter { 105 | return &mockStatter{ 106 | noop: noop, 107 | fields: log.Fields{ 108 | "module": "mock_statter", 109 | }, 110 | } 111 | } 112 | 113 | type mockStatter struct { 114 | fields log.Fields 115 | noop bool 116 | } 117 | 118 | func (s *mockStatter) Count(bucket string, n interface{}) { 119 | if s.noop { 120 | return 121 | } 122 | log.WithFields(log.WithFn(s.fields)).Debugf("Bucket %s: %v", bucket, n) 123 | } 124 | 125 | func (s *mockStatter) Increment(bucket string) { 126 | if s.noop { 127 | return 128 | } 129 | log.WithFields(log.WithFn(s.fields)).Debugf("Bucket %s", bucket) 130 | } 131 | 132 | func (s *mockStatter) Gauge(bucket string, value interface{}) { 133 | if s.noop { 134 | return 135 | } 136 | log.WithFields(log.WithFn(s.fields)).Debugf("Bucket %s: %v", bucket, value) 137 | } 138 | 139 | func (s *mockStatter) Timing(bucket string, value interface{}) { 140 | if s.noop { 141 | return 142 | } 143 | log.WithFields(log.WithFn(s.fields)).Debugf("Bucket %s: %v", bucket, value) 144 | } 145 | 146 | func (s *mockStatter) Histogram(bucket string, value interface{}) { 147 | if s.noop { 148 | return 149 | } 150 | log.WithFields(log.WithFn(s.fields)).Debugf("Bucket %s: %v", bucket, value) 151 | } 152 | 153 | func (s *mockStatter) Unique(bucket string, value string) { 154 | if s.noop { 155 | return 156 | } 157 | log.WithFields(log.WithFn(s.fields)).Debugf("Bucket %s: %v", bucket, value) 158 | } 159 | 160 | func (s *mockStatter) Close() { 161 | if s.noop { 162 | return 163 | } 164 | log.WithFields(log.WithFn(s.fields)).Debugf("closed at %s", time.Now()) 165 | } 166 | -------------------------------------------------------------------------------- /go-demo/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "strings" 7 | "time" 8 | 9 | log "github.com/xlab/suplog" 10 | ) 11 | 12 | func ReportFuncError(tags ...Tags) { 13 | fn := funcName() 14 | reportFunc(fn, "error", tags...) 15 | } 16 | 17 | func ReportClosureFuncError(name string, tags ...Tags) { 18 | reportFunc(name, "error", tags...) 19 | } 20 | 21 | func ReportFuncStatus(tags ...Tags) { 22 | fn := funcName() 23 | reportFunc(fn, "status", tags...) 24 | } 25 | 26 | func ReportClosureFuncStatus(name string, tags ...Tags) { 27 | reportFunc(name, "status", tags...) 28 | } 29 | 30 | func ReportFuncCall(tags ...Tags) { 31 | fn := funcName() 32 | reportFunc(fn, "called", tags...) 33 | } 34 | 35 | func ReportClosureFuncCall(name string, tags ...Tags) { 36 | reportFunc(name, "called", tags...) 37 | } 38 | 39 | func reportFunc(fn, action string, tags ...Tags) { 40 | clientMux.RLock() 41 | defer clientMux.RUnlock() 42 | if client == nil { 43 | return 44 | } 45 | tagSpec := joinTags(tags...) 46 | tagSpec += ",func_name=" + fn 47 | client.Increment(fmt.Sprintf("func.%v", action) + tagSpec) 48 | } 49 | 50 | type StopTimerFunc func() 51 | 52 | func ReportFuncTiming(tags ...Tags) StopTimerFunc { 53 | clientMux.RLock() 54 | defer clientMux.RUnlock() 55 | if client == nil { 56 | return func() {} 57 | } 58 | t := time.Now() 59 | fn := funcName() 60 | tagSpec := joinTags(tags...) 61 | tagSpec += ",func_name=" + fn 62 | 63 | doneC := make(chan struct{}) 64 | go func(name string, start time.Time) { 65 | select { 66 | case <-doneC: 67 | return 68 | case <-time.NewTicker(config.StuckFunctionTimeout).C: 69 | clientMux.RLock() 70 | defer clientMux.RUnlock() 71 | 72 | err := fmt.Errorf("detected stuck function: %s stuck for %v", name, time.Since(start)) 73 | log.WithError(err).Warningln("detected stuck function") 74 | client.Increment("func.stuck" + tagSpec) 75 | } 76 | }(fn, t) 77 | 78 | return func() { 79 | d := time.Since(t) 80 | close(doneC) 81 | 82 | clientMux.RLock() 83 | defer clientMux.RUnlock() 84 | client.Timing("func.timing"+tagSpec, int(d/time.Millisecond)) 85 | } 86 | } 87 | 88 | func ReportClosureFuncTiming(name string, tags ...Tags) StopTimerFunc { 89 | clientMux.RLock() 90 | defer clientMux.RUnlock() 91 | if client == nil { 92 | return func() {} 93 | } 94 | t := time.Now() 95 | tagSpec := joinTags(tags...) 96 | tagSpec += ",func_name=" + name 97 | 98 | doneC := make(chan struct{}) 99 | go func(name string, start time.Time) { 100 | select { 101 | case <-doneC: 102 | return 103 | case <-time.NewTicker(config.StuckFunctionTimeout).C: 104 | clientMux.RLock() 105 | defer clientMux.RUnlock() 106 | 107 | err := fmt.Errorf("detected stuck function: %s stuck for %v", name, time.Since(start)) 108 | log.WithError(err).Warningln("detected stuck function") 109 | client.Increment("func.stuck" + tagSpec) 110 | } 111 | }(name, t) 112 | 113 | return func() { 114 | d := time.Since(t) 115 | close(doneC) 116 | 117 | clientMux.RLock() 118 | defer clientMux.RUnlock() 119 | client.Timing("func.timing"+tagSpec, int(d/time.Millisecond)) 120 | } 121 | } 122 | 123 | func funcName() string { 124 | pc, _, _, _ := runtime.Caller(2) 125 | fullName := runtime.FuncForPC(pc).Name() 126 | parts := strings.Split(fullName, "/") 127 | nameParts := strings.Split(parts[len(parts)-1], ".") 128 | return nameParts[len(nameParts)-1] 129 | } 130 | 131 | type Tags map[string]string 132 | 133 | func (t Tags) With(k, v string) Tags { 134 | if t == nil || len(t) == 0 { 135 | return map[string]string{ 136 | k: v, 137 | } 138 | } 139 | t[k] = v 140 | return t 141 | } 142 | 143 | func joinTags(tags ...Tags) string { 144 | if len(tags) == 0 { 145 | return "" 146 | } 147 | var str string 148 | for k, v := range tags[0] { 149 | str += fmt.Sprintf(",%s=%s", k, v) 150 | } 151 | return str 152 | } 153 | -------------------------------------------------------------------------------- /go-demo/restart.sh: -------------------------------------------------------------------------------- 1 | go run cmd/go-bot-demo/main.go cmd/go-bot-demo/util.go cmd/go-bot-demo/options.go cmd/go-bot-demo/metrics.go cmd/go-bot-demo/trading.go cmd/go-bot-demo/keys.go start 2 | -------------------------------------------------------------------------------- /go-demo/trading/common.go: -------------------------------------------------------------------------------- 1 | package trading 2 | 3 | import ( 4 | "fmt" 5 | "math/big" 6 | "strconv" 7 | "time" 8 | 9 | cosmtypes "github.com/cosmos/cosmos-sdk/types" 10 | "github.com/ethereum/go-ethereum/common" 11 | "github.com/shopspring/decimal" 12 | ) 13 | 14 | func defaultSubaccount(acc cosmtypes.AccAddress) common.Hash { 15 | return common.BytesToHash(common.RightPadBytes(acc.Bytes(), 32)) 16 | } 17 | 18 | func convertDecimalIntoCosmDec(number decimal.Decimal) cosmtypes.Dec { 19 | stringNumber := number.String() 20 | cosmDecNumber := cosmtypes.MustNewDecFromStr(stringNumber) 21 | 22 | return cosmDecNumber 23 | } 24 | 25 | func convertCosmDecIntoDecimal(number cosmtypes.Dec) decimal.Decimal { 26 | stringNumber := number.String() 27 | decimalNumber, _ := decimal.NewFromString(stringNumber) 28 | 29 | return decimalNumber 30 | } 31 | 32 | // price * 10^quoteDecimals/10^baseDecimals = price * 10^(quoteDecimals - baseDecimals) 33 | // for INJ/USDT, INJ is the base which has 18 decimals and USDT is the quote which has 6 decimals 34 | func getPrice(price decimal.Decimal, baseDecimals, quoteDecimals int, minPriceTickSize cosmtypes.Dec) cosmtypes.Dec { 35 | scale := decimal.New(1, int32(quoteDecimals-baseDecimals)) 36 | priceStr := scale.Mul(price).StringFixed(18) 37 | decPrice, err := cosmtypes.NewDecFromStr(priceStr) 38 | if err != nil { 39 | fmt.Println(err.Error()) 40 | fmt.Println(priceStr, scale.String(), price.String()) 41 | fmt.Println(decPrice.String()) 42 | } 43 | realPrice := formatToTickSize(decPrice, minPriceTickSize) 44 | return realPrice 45 | } 46 | 47 | func getPriceForDerivative(price decimal.Decimal, quoteDecimals int, minPriceTickSize cosmtypes.Dec) cosmtypes.Dec { 48 | decScale := decimal.New(1, int32(quoteDecimals)) 49 | priceStr := price.Mul(decScale).StringFixed(18) 50 | mid := cosmtypes.MustNewDecFromStr(priceStr) 51 | baseDec := cosmtypes.MustNewDecFromStr("1") 52 | scale := baseDec.Quo(minPriceTickSize) // from tick size to coin size 53 | midScaledInt := mid.Mul(scale).TruncateDec() 54 | realPrice := minPriceTickSize.Mul(midScaledInt) 55 | return realPrice 56 | } 57 | 58 | func formatToTickSize(value, tickSize cosmtypes.Dec) cosmtypes.Dec { 59 | residue := new(big.Int).Mod(value.BigInt(), tickSize.BigInt()) 60 | formattedValue := new(big.Int).Sub(value.BigInt(), residue) 61 | p := decimal.NewFromBigInt(formattedValue, -18).StringFixed(18) 62 | realValue, _ := cosmtypes.NewDecFromStr(p) 63 | return realValue 64 | } 65 | 66 | // convert decimal.Decimal into acceptable quantity, (input value's unit is coin, ex: 5 inj) 67 | func getQuantity(value decimal.Decimal, minTickSize cosmtypes.Dec, baseDecimals int) (qty cosmtypes.Dec) { 68 | mid, _ := cosmtypes.NewDecFromStr(value.String()) 69 | bStr := decimal.New(1, int32(baseDecimals)).StringFixed(18) 70 | baseDec, _ := cosmtypes.NewDecFromStr(bStr) 71 | scale := baseDec.Quo(minTickSize) // from tick size to coin size 72 | midScaledInt := mid.Mul(scale).TruncateDec() 73 | qty = minTickSize.Mul(midScaledInt) 74 | return qty 75 | } 76 | 77 | // convert decimal.Decimal into acceptable quantity, (input value's unit is coin, ex: 5 inj) 78 | func getQuantityForDerivative(value decimal.Decimal, minTickSize cosmtypes.Dec, quoteDecimals int) (qty cosmtypes.Dec) { 79 | mid, _ := cosmtypes.NewDecFromStr(value.String()) 80 | baseDec := cosmtypes.MustNewDecFromStr("1") 81 | scale := baseDec.Quo(minTickSize) // from tick size to coin size 82 | midScaledInt := mid.Mul(scale).TruncateDec() 83 | qty = minTickSize.Mul(midScaledInt) 84 | return qty 85 | } 86 | 87 | //convert cosmostype.Dec into readable string price 88 | func getPriceForPrintOut(price cosmtypes.Dec, baseDecimals, quoteDecimals int) (priceOut float64) { 89 | scale := decimal.New(1, int32(baseDecimals-quoteDecimals)).StringFixed(18) 90 | scaleCos, _ := cosmtypes.NewDecFromStr(scale) 91 | priceStr := price.Mul(scaleCos).String() 92 | priceOut, _ = strconv.ParseFloat(priceStr, 64) 93 | return priceOut 94 | } 95 | 96 | //convert cosmostype.Dec into readable string price 97 | func getPriceForPrintOutForDerivative(price cosmtypes.Dec, quoteDecimals int) (priceOut float64) { 98 | scale := decimal.New(1, int32(-quoteDecimals)).StringFixed(18) 99 | scaleCos, _ := cosmtypes.NewDecFromStr(scale) 100 | priceStr := price.Mul(scaleCos).String() 101 | priceOut, _ = strconv.ParseFloat(priceStr, 64) 102 | return priceOut 103 | } 104 | 105 | func formatQuantityToTickSize(value, minTickSize cosmtypes.Dec) cosmtypes.Dec { 106 | modi := value.Quo(minTickSize).TruncateDec() 107 | qty := modi.Mul(minTickSize) 108 | return qty 109 | } 110 | 111 | // round function for float64 112 | func Round(v float64, decimals int) float64 { 113 | place := int32(decimals) 114 | x, _ := decimal.NewFromFloat(v).Round(place).Float64() 115 | return x 116 | } 117 | 118 | func TimeOfNextUTCDay() time.Time { 119 | now := time.Now().UTC() 120 | nextUTCday := now.Add(time.Hour * 24) 121 | nextUTCday = time.Date(nextUTCday.Year(), nextUTCday.Month(), nextUTCday.Day(), 0, 0, 0, 0, nextUTCday.Location()) 122 | return nextUTCday 123 | } 124 | 125 | //convert cosmostype.Dec into readable decimal price 126 | func getPriceIntoDecimalForDerivative(price cosmtypes.Dec, quoteDecimals int) (priceOut decimal.Decimal) { 127 | scale := decimal.New(1, int32(-quoteDecimals)).String() 128 | scaleCos, _ := cosmtypes.NewDecFromStr(scale) 129 | priceStr := price.Mul(scaleCos).String() 130 | priceOut, _ = decimal.NewFromString(priceStr) 131 | return priceOut 132 | } -------------------------------------------------------------------------------- /go-demo/trading/common_derivatives.go: -------------------------------------------------------------------------------- 1 | package trading 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | exchangetypes "github.com/InjectiveLabs/sdk-go/chain/exchange/types" 8 | oracletypes "github.com/InjectiveLabs/sdk-go/chain/oracle/types" 9 | derivativeExchangePB "github.com/InjectiveLabs/sdk-go/exchange/derivative_exchange_rpc/pb" 10 | cosmtypes "github.com/cosmos/cosmos-sdk/types" 11 | "github.com/ethereum/go-ethereum/common" 12 | "github.com/shopspring/decimal" 13 | ) 14 | 15 | const DerivativeBookSideOrderCount = 4 16 | 17 | type DerivativeOrderData struct { 18 | OrderType exchangetypes.OrderType 19 | Price cosmtypes.Dec 20 | Quantity cosmtypes.Dec 21 | Margin cosmtypes.Dec 22 | FeeRecipient string 23 | } 24 | type DerivativeMarketInfo struct { 25 | Ticker string 26 | QuoteDenom string 27 | QuoteDenomDecimals int 28 | OracleBase string 29 | OracleQuote string 30 | OracleType oracletypes.OracleType 31 | OracleScaleFactor uint32 32 | IsPerpetual bool 33 | MarketID common.Hash 34 | MarkPrice cosmtypes.Dec 35 | MinPriceTickSize cosmtypes.Dec 36 | MinQuantityTickSize cosmtypes.Dec 37 | BaseSymbol string 38 | QuoteSymbol string 39 | // balance info 40 | //BaseBalances InjectiveBaseAssetsBranch 41 | AccountValue decimal.Decimal 42 | // risk controls 43 | PositionDirection string 44 | PositionQty decimal.Decimal 45 | MarginValue decimal.Decimal 46 | LastSendMessageTime []time.Time 47 | ReachMaxPosition bool 48 | // for market making 49 | LocalOrderBook []interface{} 50 | OraclePrice decimal.Decimal 51 | OracleReady bool 52 | CosmOraclePrice cosmtypes.Dec 53 | MaxOrderSize decimal.Decimal 54 | MaxOrderValue decimal.Decimal 55 | TopBidPrice cosmtypes.Dec 56 | TopAskPrice cosmtypes.Dec 57 | 58 | OrderMain OrderMaintainBranch 59 | // as model 60 | MinVolatilityPCT float64 61 | MarketVolatility decimal.Decimal 62 | InventoryPCT decimal.Decimal 63 | FillIndensity float64 64 | RiskAversion float64 65 | ReservedPrice decimal.Decimal 66 | OptSpread decimal.Decimal 67 | // staking orders 68 | BestAsk decimal.Decimal 69 | BestBid decimal.Decimal 70 | // orders 71 | BidOrders OrdersBranch 72 | AskOrders OrdersBranch 73 | } 74 | 75 | type OrderMaintainBranch struct { 76 | mux sync.RWMutex 77 | Status string 78 | Updated bool 79 | LastSend time.Time 80 | } 81 | 82 | type CumFilledBranch struct { 83 | mux sync.RWMutex 84 | Amount decimal.Decimal 85 | LastCheckTime time.Time 86 | } 87 | 88 | type OrdersBranch struct { 89 | mux sync.RWMutex // read write lock 90 | Orders []*derivativeExchangePB.DerivativeLimitOrder 91 | } 92 | 93 | func NewDerivativeOrder(subaccountID common.Hash, m *DerivativeMarketInfo, d *DerivativeOrderData) *exchangetypes.DerivativeOrder { 94 | 95 | return &exchangetypes.DerivativeOrder{ 96 | MarketId: m.MarketID.Hex(), 97 | OrderType: d.OrderType, 98 | OrderInfo: exchangetypes.OrderInfo{ 99 | SubaccountId: subaccountID.Hex(), 100 | FeeRecipient: d.FeeRecipient, 101 | Price: d.Price, 102 | Quantity: d.Quantity, 103 | }, 104 | Margin: d.Margin, 105 | } 106 | } 107 | 108 | func DerivativeMarketToMarketInfo(m *derivativeExchangePB.DerivativeMarketInfo) *DerivativeMarketInfo { 109 | 110 | var quoteDenomDecimals int 111 | 112 | if m.QuoteTokenMeta != nil { 113 | quoteDenomDecimals = int(m.QuoteTokenMeta.Decimals) 114 | } else { 115 | quoteDenomDecimals = 6 116 | } 117 | oracleType, _ := oracletypes.GetOracleType(m.OracleType) 118 | minPriceTickSize, _ := cosmtypes.NewDecFromStr(m.MinPriceTickSize) 119 | minQuantityTickSize, _ := cosmtypes.NewDecFromStr(m.MinQuantityTickSize) 120 | return &DerivativeMarketInfo{ 121 | Ticker: m.Ticker, 122 | QuoteDenom: m.QuoteDenom, 123 | QuoteDenomDecimals: quoteDenomDecimals, 124 | OracleBase: m.OracleBase, 125 | OracleQuote: m.OracleQuote, 126 | OracleType: oracleType, 127 | OracleScaleFactor: m.OracleScaleFactor, 128 | IsPerpetual: m.IsPerpetual, 129 | MarketID: common.HexToHash(m.MarketId), 130 | MinPriceTickSize: minPriceTickSize, 131 | MinQuantityTickSize: minQuantityTickSize, 132 | } 133 | } 134 | 135 | func (m *DerivativeMarketInfo) UpdateOrderMain(status string) { 136 | m.OrderMain.mux.Lock() 137 | defer m.OrderMain.mux.Unlock() 138 | m.OrderMain.Status = status 139 | m.OrderMain.LastSend = time.Now() 140 | m.OrderMain.Updated = false 141 | } 142 | 143 | func (m *DerivativeMarketInfo) MaintainOrderStatus(new int) { 144 | m.OrderMain.mux.Lock() 145 | defer m.OrderMain.mux.Unlock() 146 | if time.Now().After(m.OrderMain.LastSend.Add(time.Second * 15)) { 147 | m.OrderMain.Updated = true 148 | m.OrderMain.Status = Wait 149 | } else { 150 | orignal := m.GetBidOrdersLen() + m.GetAskOrdersLen() 151 | switch m.OrderMain.Status { 152 | case Add: 153 | if orignal < new { 154 | m.OrderMain.Updated = true 155 | } 156 | case Reduce: 157 | if orignal > new { 158 | m.OrderMain.Updated = true 159 | } 160 | } 161 | } 162 | } 163 | 164 | func (m *DerivativeMarketInfo) IsOrderUpdated() bool { 165 | m.OrderMain.mux.RLock() 166 | defer m.OrderMain.mux.RUnlock() 167 | return m.OrderMain.Updated 168 | } 169 | -------------------------------------------------------------------------------- /go-demo/trading/inj_balances.go: -------------------------------------------------------------------------------- 1 | package trading 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | "time" 9 | 10 | accountsPB "github.com/InjectiveLabs/sdk-go/exchange/accounts_rpc/pb" 11 | spotExchangePB "github.com/InjectiveLabs/sdk-go/exchange/spot_exchange_rpc/pb" 12 | 13 | "github.com/shopspring/decimal" 14 | ) 15 | 16 | func (s *tradingSvc) UpdateInjectiveSpotAccountSession(ctx context.Context, interval time.Duration) { 17 | AccountCheck := time.NewTicker(time.Second * interval) 18 | defer AccountCheck.Stop() 19 | for { 20 | select { 21 | case <-ctx.Done(): 22 | return 23 | case <-AccountCheck.C: 24 | err := s.GetInjectiveSpotAccount(ctx) 25 | if err != nil { 26 | message := fmt.Sprintf("❌ Failed to get Injective account with err: %s, resend in 10min if still not working\n", err.Error()) 27 | s.logger.Errorln(message) 28 | continue 29 | continue 30 | } 31 | s.UpdateInjectiveAccountBalances() 32 | // recording usdt values 33 | totalUSDT := s.GetInjectiveTotalQuoteBalance("USDT") 34 | overallValue := totalUSDT 35 | s.logger.Infof("MM strategy has %s USDT in total.", overallValue.Round(2).String()) 36 | default: 37 | time.Sleep(time.Second) 38 | } 39 | } 40 | } 41 | 42 | func (s *tradingSvc) GetInjectiveSpotAccount(ctx context.Context) error { 43 | sender := s.cosmosClient.FromAddress() 44 | subaccountID := defaultSubaccount(sender) 45 | resp, err := s.accountsClient.SubaccountBalancesList(ctx, &accountsPB.SubaccountBalancesListRequest{ 46 | SubaccountId: subaccountID.Hex(), 47 | }) 48 | if err != nil { 49 | return err 50 | } 51 | if resp == nil { 52 | return errors.New("get nil response from GetInjectiveSpotAccount.") 53 | } 54 | s.dataCenter.InjectiveSpotAccount.mux.Lock() 55 | s.dataCenter.InjectiveSpotAccount.res = resp 56 | s.dataCenter.InjectiveSpotAccount.mux.Unlock() 57 | return nil 58 | } 59 | 60 | func (s *tradingSvc) InitialInjAssetWithDenom(quotes []string, resp *spotExchangePB.MarketsResponse) { 61 | // get asset denom from resp for easier mananging 62 | for _, quote := range quotes { 63 | for _, market := range resp.Markets { 64 | asset := strings.Split(market.Ticker, "/")[1] 65 | if quote == asset { 66 | if !s.IsAssetAlreadyInSliceOfInjSpotAccount(asset) { 67 | s.dataCenter.InjectiveSpotAccount.Assets = append(s.dataCenter.InjectiveSpotAccount.Assets, asset) 68 | s.dataCenter.InjectiveSpotAccount.Denoms = append(s.dataCenter.InjectiveSpotAccount.Denoms, market.QuoteDenom) 69 | } 70 | break 71 | } 72 | } 73 | } 74 | l := len(s.dataCenter.InjectiveSpotAccount.Assets) 75 | s.dataCenter.InjectiveSpotAccount.AvailableAmounts = make([]decimal.Decimal, l) 76 | s.dataCenter.InjectiveSpotAccount.TotalAmounts = make([]decimal.Decimal, l) 77 | s.dataCenter.InjectiveSpotAccount.LockedValues = make([]decimal.Decimal, l) 78 | s.dataCenter.InjectiveSpotAccount.TotalValues = make([]decimal.Decimal, l) 79 | } 80 | 81 | func (s *tradingSvc) IsAssetAlreadyInSliceOfInjSpotAccount(asset string) bool { 82 | for _, data := range s.dataCenter.InjectiveSpotAccount.Assets { 83 | if data == asset { 84 | return true 85 | } 86 | } 87 | return false 88 | } 89 | 90 | func (s *tradingSvc) InitialInjectiveAccountBalances(assets []string, resp *spotExchangePB.MarketsResponse) { 91 | // get quote denom from resp 92 | for _, asset := range assets { 93 | for _, market := range resp.Markets { 94 | tmp := strings.Split(market.Ticker, "/") 95 | if len(tmp) != 2 { 96 | continue 97 | } 98 | quoteAsset := tmp[1] 99 | if asset == quoteAsset { 100 | var quoteDenomDecimals int 101 | if market.QuoteTokenMeta != nil { 102 | quoteDenomDecimals = int(market.QuoteTokenMeta.Decimals) 103 | } else { 104 | quoteDenomDecimals = 6 105 | } 106 | data := InjectiveQuoteAssetsBranch{ 107 | Asset: asset, 108 | Denom: market.QuoteDenom, 109 | QuoteDenomDecimals: quoteDenomDecimals, 110 | } 111 | s.dataCenter.InjectiveQuoteAssets = append(s.dataCenter.InjectiveQuoteAssets, data) 112 | break 113 | } 114 | } 115 | } 116 | s.dataCenter.InjectiveQuoteLen = len(s.dataCenter.InjectiveQuoteAssets) 117 | // update balances data 118 | s.UpdateInjectiveAccountBalances() 119 | } 120 | 121 | func (s *tradingSvc) UpdateInjectiveAccountBalances() { 122 | for i := 0; i < s.dataCenter.InjectiveQuoteLen; i++ { 123 | for _, item := range s.dataCenter.InjectiveSpotAccount.res.Balances { 124 | if s.dataCenter.InjectiveQuoteAssets[i].Denom == item.Denom { 125 | s.dataCenter.InjectiveQuoteAssets[i].mux.Lock() 126 | avaiB, _ := decimal.NewFromString(item.Deposit.AvailableBalance) 127 | s.dataCenter.InjectiveQuoteAssets[i].AvailableBalance = decimal.NewFromBigInt(avaiB.BigInt(), int32(-s.dataCenter.InjectiveQuoteAssets[i].QuoteDenomDecimals)) 128 | totalB, _ := decimal.NewFromString(item.Deposit.TotalBalance) 129 | s.dataCenter.InjectiveQuoteAssets[i].TotalBalance = decimal.NewFromBigInt(totalB.BigInt(), int32(-s.dataCenter.InjectiveQuoteAssets[i].QuoteDenomDecimals)) 130 | s.dataCenter.InjectiveQuoteAssets[i].mux.Unlock() 131 | for j, denom := range s.dataCenter.InjectiveSpotAccount.Denoms { 132 | if denom == s.dataCenter.InjectiveQuoteAssets[i].Denom { 133 | s.dataCenter.InjectiveSpotAccount.AvailableAmounts[j] = s.dataCenter.InjectiveQuoteAssets[i].AvailableBalance 134 | s.dataCenter.InjectiveSpotAccount.TotalAmounts[j] = s.dataCenter.InjectiveQuoteAssets[i].TotalBalance 135 | s.dataCenter.InjectiveSpotAccount.LockedValues[j] = s.dataCenter.InjectiveQuoteAssets[i].TotalBalance.Sub(s.dataCenter.InjectiveQuoteAssets[i].AvailableBalance) 136 | s.dataCenter.InjectiveSpotAccount.TotalValues[j] = s.dataCenter.InjectiveSpotAccount.TotalAmounts[j] 137 | break 138 | } 139 | } 140 | } 141 | } 142 | } 143 | } 144 | 145 | // input asset name to get shared quote asset balance amount 146 | func (s *tradingSvc) GetInjectiveAvailableQuoteBalance(asset string) (available decimal.Decimal) { 147 | for i := 0; i < s.dataCenter.InjectiveQuoteLen; i++ { 148 | if s.dataCenter.InjectiveQuoteAssets[i].Asset == asset { 149 | s.dataCenter.InjectiveQuoteAssets[i].mux.RLock() 150 | available = s.dataCenter.InjectiveQuoteAssets[i].AvailableBalance 151 | s.dataCenter.InjectiveQuoteAssets[i].mux.RUnlock() 152 | } 153 | } 154 | return available 155 | } 156 | 157 | // input asset name to get shared quote asset total balance amount 158 | func (s *tradingSvc) GetInjectiveTotalQuoteBalance(asset string) (total decimal.Decimal) { 159 | for i := 0; i < s.dataCenter.InjectiveQuoteLen; i++ { 160 | if s.dataCenter.InjectiveQuoteAssets[i].Asset == asset { 161 | s.dataCenter.InjectiveQuoteAssets[i].mux.RLock() 162 | total = s.dataCenter.InjectiveQuoteAssets[i].TotalBalance 163 | s.dataCenter.InjectiveQuoteAssets[i].mux.RUnlock() 164 | } 165 | } 166 | return total 167 | } 168 | 169 | // add asset amount to shared quote asset balance 170 | func (s *tradingSvc) AddInjectiveAvailableQuoteBalance(asset string, amount decimal.Decimal) { 171 | for i := 0; i < s.dataCenter.InjectiveQuoteLen; i++ { 172 | if s.dataCenter.InjectiveQuoteAssets[i].Asset == asset { 173 | s.dataCenter.InjectiveQuoteAssets[i].mux.Lock() 174 | s.dataCenter.InjectiveQuoteAssets[i].AvailableBalance = s.dataCenter.InjectiveQuoteAssets[i].AvailableBalance.Add(amount) 175 | s.dataCenter.InjectiveQuoteAssets[i].mux.Unlock() 176 | } 177 | } 178 | } 179 | 180 | // sub asset amount to shared quote asset balance 181 | func (s *tradingSvc) SubInjectiveAvailableQuoteBalance(asset string, amount decimal.Decimal) { 182 | for i := 0; i < s.dataCenter.InjectiveQuoteLen; i++ { 183 | if s.dataCenter.InjectiveQuoteAssets[i].Asset == asset { 184 | s.dataCenter.InjectiveQuoteAssets[i].mux.Lock() 185 | s.dataCenter.InjectiveQuoteAssets[i].AvailableBalance = s.dataCenter.InjectiveQuoteAssets[i].AvailableBalance.Sub(amount) 186 | s.dataCenter.InjectiveQuoteAssets[i].mux.Unlock() 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /go-demo/trading/inj_position.go: -------------------------------------------------------------------------------- 1 | package trading 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "time" 9 | 10 | exchangetypes "github.com/InjectiveLabs/sdk-go/chain/exchange/types" 11 | derivativeExchangePB "github.com/InjectiveLabs/sdk-go/exchange/derivative_exchange_rpc/pb" 12 | cosmtypes "github.com/cosmos/cosmos-sdk/types" 13 | "github.com/shopspring/decimal" 14 | ) 15 | 16 | func (s *tradingSvc) GetInjectivePositions(ctx context.Context) error { 17 | sender := s.cosmosClient.FromAddress() 18 | subaccountID := defaultSubaccount(sender) 19 | resp, err := s.derivativesClient.Positions(ctx, &derivativeExchangePB.PositionsRequest{ 20 | SubaccountId: subaccountID.Hex(), 21 | }) 22 | if err != nil { 23 | s.logger.Infof("Failed to get derivative positions") 24 | return err 25 | } 26 | if resp == nil { 27 | return errors.New("get nil response from GetInjectivePositions.") 28 | } 29 | s.dataCenter.InjectivePositions.mux.Lock() 30 | s.dataCenter.InjectivePositions.res = resp 31 | s.dataCenter.InjectivePositions.mux.Unlock() 32 | return nil 33 | } 34 | 35 | func (s *tradingSvc) UpdateInjectivePositionSession(ctx context.Context, interval time.Duration) { 36 | var lastSend time.Time 37 | PositionCheck := time.NewTicker(time.Second * interval) 38 | defer PositionCheck.Stop() 39 | for { 40 | select { 41 | case <-ctx.Done(): 42 | return 43 | case <-PositionCheck.C: 44 | err := s.GetInjectivePositions(ctx) 45 | if err != nil { 46 | message := fmt.Sprintf("❌ Failed to get Injective positions with err: %s, resend in 10min if still not working\n", err.Error()) 47 | if !time.Now().After(lastSend.Add(time.Second * 600)) { 48 | s.logger.Errorln(message) 49 | continue 50 | } 51 | lastSend = time.Now() 52 | continue 53 | } 54 | s.logger.Infoln("Updated Injective positions data") 55 | default: 56 | time.Sleep(time.Second) 57 | } 58 | } 59 | } 60 | 61 | func (s *tradingSvc) IsAlreadyInPositions(ticker string) bool { 62 | sender := s.cosmosClient.FromAddress() 63 | subaccountID := defaultSubaccount(sender) 64 | s.dataCenter.InjectivePositions.mux.RLock() 65 | defer s.dataCenter.InjectivePositions.mux.RUnlock() 66 | for _, pos := range s.dataCenter.InjectivePositions.res.Positions { 67 | if pos.Ticker == ticker && pos.SubaccountId == subaccountID.Hex() { 68 | return true 69 | } 70 | } 71 | return false 72 | } 73 | 74 | func (s *tradingSvc) GetInjectivePositionData(ticker string) *derivativeExchangePB.DerivativePosition { 75 | sender := s.cosmosClient.FromAddress() 76 | subaccountID := defaultSubaccount(sender) 77 | s.dataCenter.InjectivePositions.mux.RLock() 78 | defer s.dataCenter.InjectivePositions.mux.RUnlock() 79 | for _, pos := range s.dataCenter.InjectivePositions.res.Positions { 80 | if pos.Ticker == ticker && pos.SubaccountId == subaccountID.Hex() { 81 | return pos 82 | } 83 | } 84 | return nil 85 | } 86 | 87 | func (s *tradingSvc) AddNewInjectivePosition(pos *derivativeExchangePB.DerivativePosition) { 88 | sender := s.cosmosClient.FromAddress() 89 | subaccountID := defaultSubaccount(sender) 90 | if pos.SubaccountId != subaccountID.Hex() { 91 | return 92 | } 93 | s.dataCenter.InjectivePositions.mux.Lock() 94 | defer s.dataCenter.InjectivePositions.mux.Unlock() 95 | s.dataCenter.InjectivePositions.res.Positions = append(s.dataCenter.InjectivePositions.res.Positions, pos) 96 | } 97 | 98 | func (s *tradingSvc) UpdateInjectivePosition(pos *derivativeExchangePB.DerivativePosition) { 99 | s.dataCenter.InjectivePositions.mux.Lock() 100 | defer s.dataCenter.InjectivePositions.mux.Unlock() 101 | for i, oldPos := range s.dataCenter.InjectivePositions.res.Positions { 102 | if oldPos.Ticker == pos.Ticker { 103 | s.dataCenter.InjectivePositions.res.Positions[i] = pos 104 | } 105 | } 106 | } 107 | 108 | func (s *tradingSvc) DeleteInjectivePosition(ticker string) { 109 | s.dataCenter.InjectivePositions.mux.Lock() 110 | defer s.dataCenter.InjectivePositions.mux.Unlock() 111 | for i, oldPos := range s.dataCenter.InjectivePositions.res.Positions { 112 | if oldPos.Ticker == ticker { 113 | s.dataCenter.InjectivePositions.res.Positions = append(s.dataCenter.InjectivePositions.res.Positions[:i], s.dataCenter.InjectivePositions.res.Positions[i+1:]...) 114 | } 115 | } 116 | } 117 | 118 | func (s *tradingSvc) IncreasePositionMargin(amount decimal.Decimal, m *derivativeExchangePB.DerivativeMarketInfo) { 119 | CosAmount := cosmtypes.MustNewDecFromStr(amount.String()) 120 | sender := s.cosmosClient.FromAddress() 121 | subaccountID := defaultSubaccount(sender) 122 | msgs := make([]cosmtypes.Msg, 0, 10) 123 | msg := &exchangetypes.MsgIncreasePositionMargin{ 124 | Sender: sender.String(), 125 | MarketId: m.MarketId, 126 | SourceSubaccountId: subaccountID.Hex(), 127 | DestinationSubaccountId: subaccountID.Hex(), 128 | Amount: CosAmount, 129 | } 130 | msgs = append(msgs, msg) 131 | if _, err := s.cosmosClient.AsyncBroadcastMsg(msgs...); err != nil { 132 | var buffer bytes.Buffer 133 | buffer.WriteString("fail to increase position margin for ") 134 | buffer.WriteString(m.Ticker) 135 | buffer.WriteString(", probably not enough margin for the positions, go check it up!") 136 | message := buffer.String() 137 | s.logger.Warningln(message) 138 | } else { 139 | s.logger.Infof("Increased position margin.") 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /go-demo/trading/inj_stream.go: -------------------------------------------------------------------------------- 1 | package trading 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | derivativeExchangePB "github.com/InjectiveLabs/sdk-go/exchange/derivative_exchange_rpc/pb" 10 | oraclePB "github.com/InjectiveLabs/sdk-go/exchange/oracle_rpc/pb" 11 | cosmtypes "github.com/cosmos/cosmos-sdk/types" 12 | "github.com/ethereum/go-ethereum/common" 13 | "github.com/shopspring/decimal" 14 | ) 15 | 16 | // stream key words 17 | const ( 18 | Insert = "insert" 19 | Update = "update" 20 | Unfilled = "unfilled" 21 | Booked = "booked" 22 | Canceled = "canceled" 23 | Partial = "partial_filled" 24 | ) 25 | 26 | func (s *tradingSvc) StreamInjectiveDerivativeOrderSession(ctx context.Context, m *derivativeExchangePB.DerivativeMarketInfo, subaccountID *common.Hash, marketInfo *DerivativeMarketInfo) { 27 | for { 28 | select { 29 | case <-ctx.Done(): 30 | s.logger.Infof("Closing %s derivative order session.", m.Ticker) 31 | return 32 | default: 33 | s.StreamInjectiveDerivativeOrder(ctx, m, subaccountID, marketInfo) 34 | message := fmt.Sprintf("Reconnecting %s derivative StreamOrders...", m.Ticker) 35 | s.logger.Errorln(message) 36 | } 37 | } 38 | } 39 | 40 | func (s *tradingSvc) StreamInjectiveDerivativeOrder(ctx context.Context, m *derivativeExchangePB.DerivativeMarketInfo, subaccountID *common.Hash, marketInfo *DerivativeMarketInfo) { 41 | steam, err := s.derivativesClient.StreamOrders(ctx, &derivativeExchangePB.StreamOrdersRequest{ 42 | MarketId: m.MarketId, 43 | SubaccountId: subaccountID.Hex(), 44 | }) 45 | if err != nil { 46 | message := fmt.Sprintf("Fail to connect derivative StreamTrades for %s with err: %s", m.Ticker, err.Error()) 47 | s.logger.Errorln(message) 48 | time.Sleep(time.Second * 10) 49 | return 50 | } 51 | s.logger.Infof("Connected to %s derivative StreamOrders", m.Ticker) 52 | for { 53 | select { 54 | case <-ctx.Done(): 55 | return 56 | default: 57 | resp, err := steam.Recv() 58 | if err != nil { 59 | message := fmt.Sprintf("Closing %s derivative StreamOrders with err: %s", m.Ticker, err.Error()) 60 | s.logger.Errorln(message) 61 | return 62 | } 63 | go marketInfo.HandleOrder(resp) 64 | } 65 | } 66 | } 67 | 68 | func (m *DerivativeMarketInfo) HandleOrder(resp *derivativeExchangePB.StreamOrdersResponse) { 69 | switch strings.ToLower(resp.OperationType) { 70 | case Insert: 71 | switch strings.ToLower(resp.Order.State) { 72 | case Unfilled: 73 | // insert new limit order 74 | switch strings.ToLower(resp.Order.OrderSide) { 75 | case "sell": 76 | m.InsertAskOrders(resp.Order) 77 | case "buy": 78 | m.InsertBidOrders(resp.Order) 79 | } 80 | case Booked: 81 | // mainnet 82 | switch strings.ToLower(resp.Order.OrderSide) { 83 | case "sell": 84 | m.InsertAskOrders(resp.Order) 85 | case "buy": 86 | m.InsertBidOrders(resp.Order) 87 | } 88 | } 89 | case Update: 90 | if strings.ToLower(resp.Order.State) == Canceled { 91 | // insert new limit order 92 | switch strings.ToLower(resp.Order.OrderSide) { 93 | case "sell": 94 | if m.IsMyAskOrder(resp.Order.OrderHash) { 95 | m.CancelAskOrdersFromOrderList([]*derivativeExchangePB.DerivativeLimitOrder{resp.Order}) 96 | } 97 | case "buy": 98 | if m.IsMyBidOrder(resp.Order.OrderHash) { 99 | m.CancelBidOrdersFromOrderList([]*derivativeExchangePB.DerivativeLimitOrder{resp.Order}) 100 | } 101 | } 102 | } else if strings.ToLower(resp.Order.State) == Partial { 103 | switch strings.ToLower(resp.Order.OrderSide) { 104 | case "sell": 105 | m.UpdateAskOrders(resp.Order) 106 | case "buy": 107 | m.UpdateBidOrders(resp.Order) 108 | } 109 | } 110 | default: 111 | // pass 112 | } 113 | } 114 | 115 | func (s *tradingSvc) StreamInjectiveDerivativeTradeSession(ctx context.Context, m *derivativeExchangePB.DerivativeMarketInfo, subaccountID *common.Hash, marketInfo *DerivativeMarketInfo) { 116 | for { 117 | select { 118 | case <-ctx.Done(): 119 | s.logger.Infof("Closing %s derivative trade session.", m.Ticker) 120 | return 121 | default: 122 | s.StreamInjectiveDerivativeTrade(ctx, m, subaccountID, marketInfo) 123 | message := fmt.Sprintf("Reconnecting %s derivative StreamTrades...", m.Ticker) 124 | s.logger.Errorln(message) 125 | } 126 | } 127 | } 128 | 129 | func (s *tradingSvc) StreamInjectiveDerivativeTrade(ctx context.Context, m *derivativeExchangePB.DerivativeMarketInfo, subaccountID *common.Hash, marketInfo *DerivativeMarketInfo) { 130 | steam, err := s.derivativesClient.StreamTrades(ctx, &derivativeExchangePB.StreamTradesRequest{ 131 | MarketId: m.MarketId, 132 | SubaccountId: subaccountID.Hex(), 133 | }) 134 | if err != nil { 135 | message := fmt.Sprintf("Fail to connect derivative StreamTrades for %s with err: %s", m.Ticker, err.Error()) 136 | s.logger.Errorln(message) 137 | time.Sleep(time.Second * 10) 138 | return 139 | } 140 | s.logger.Infof("Connected to %s derivative StreamTrades", m.Ticker) 141 | for { 142 | select { 143 | case <-ctx.Done(): 144 | return 145 | default: 146 | resp, err := steam.Recv() 147 | if err != nil { 148 | message := fmt.Sprintf("Closing %s derivative StreamTrades with err: %s", m.Ticker, err.Error()) 149 | s.logger.Errorln(message) 150 | return 151 | } 152 | go s.HandleTrades(m.Ticker, resp, marketInfo) 153 | } 154 | } 155 | } 156 | 157 | func (s *tradingSvc) HandleTrades(ticker string, resp *derivativeExchangePB.StreamTradesResponse, marketInfo *DerivativeMarketInfo) { 158 | if resp == nil { 159 | return 160 | } 161 | data := resp.Trade 162 | qty, _ := decimal.NewFromString(data.PositionDelta.ExecutionQuantity) 163 | if qty.IsZero() { 164 | return 165 | } 166 | tradeSide := strings.ToLower(data.PositionDelta.TradeDirection) 167 | cosPrice, _ := cosmtypes.NewDecFromStr(data.PositionDelta.ExecutionPrice) 168 | tradePrice := getPriceForPrintOutForDerivative(cosPrice, marketInfo.QuoteDenomDecimals) 169 | tradeQty, _ := qty.Float64() 170 | execType := strings.ToLower(data.TradeExecutionType) 171 | stamp := time.Unix(0, data.ExecutedAt*int64(1000000)) 172 | 173 | if tradePrice*tradeQty > 15 { 174 | message := fmt.Sprintf("%s ✅ %s filled %s derivative order @ %f with %f %s on Inj-Exch", stamp.Format("2006-01-02 15:04:05.999"), execType, tradeSide, tradePrice, tradeQty, ticker) 175 | s.logger.Infof(message) 176 | } 177 | } 178 | 179 | func (s *tradingSvc) StreamInjectiveDerivativePositionSession(ctx context.Context, m *derivativeExchangePB.DerivativeMarketInfo, subaccountID *common.Hash, marketInfo *DerivativeMarketInfo) { 180 | for { 181 | select { 182 | case <-ctx.Done(): 183 | s.logger.Infof("Closing %s derivative position session.", m.Ticker) 184 | return 185 | default: 186 | s.StreamInjectiveDerivativePositions(ctx, m, subaccountID, marketInfo) 187 | message := fmt.Sprintf("Reconnecting %s derivative StreamPositions...", m.Ticker) 188 | s.logger.Errorln(message) 189 | } 190 | } 191 | } 192 | 193 | func (s *tradingSvc) StreamInjectiveDerivativePositions(ctx context.Context, m *derivativeExchangePB.DerivativeMarketInfo, subaccountID *common.Hash, marketInfo *DerivativeMarketInfo) { 194 | steam, err := s.derivativesClient.StreamPositions(ctx, &derivativeExchangePB.StreamPositionsRequest{ 195 | MarketId: m.MarketId, 196 | SubaccountId: subaccountID.Hex(), 197 | }) 198 | if err != nil { 199 | message := fmt.Sprintf("Fail to connect derivative StreamPositions for %s with err: %s", m.Ticker, err.Error()) 200 | s.logger.Errorln(message) 201 | time.Sleep(time.Second * 10) 202 | return 203 | } 204 | s.logger.Infof("Connected to %s derivative StreamPositions", m.Ticker) 205 | for { 206 | select { 207 | case <-ctx.Done(): 208 | return 209 | default: 210 | resp, err := steam.Recv() 211 | if err != nil { 212 | message := fmt.Sprintf("Closing %s derivative StreamPositions with err: %s", m.Ticker, err.Error()) 213 | s.logger.Errorln(message) 214 | return 215 | } 216 | go s.HandlePosition(resp, m.Ticker) 217 | } 218 | } 219 | } 220 | 221 | func (s *tradingSvc) HandlePosition(resp *derivativeExchangePB.StreamPositionsResponse, ticker string) { 222 | data := resp.Position 223 | if data == nil { 224 | s.DeleteInjectivePosition(ticker) 225 | return 226 | } 227 | if s.IsAlreadyInPositions(data.Ticker) { 228 | // update position 229 | s.UpdateInjectivePosition(data) 230 | return 231 | } 232 | // new position 233 | s.AddNewInjectivePosition(data) 234 | } 235 | 236 | func (s *tradingSvc) StreamInjectiveOraclePriceSession(ctx context.Context, m *derivativeExchangePB.DerivativeMarketInfo, marketInfo *DerivativeMarketInfo) { 237 | for { 238 | select { 239 | case <-ctx.Done(): 240 | s.logger.Infof("Closing %s oracle price session.", m.Ticker) 241 | return 242 | default: 243 | s.StreamInjectiveOraclePrices(ctx, m, marketInfo) 244 | message := fmt.Sprintf("Reconnecting %s oracle StreamPrices...", m.Ticker) 245 | s.logger.Errorln(message) 246 | } 247 | } 248 | } 249 | 250 | func (s *tradingSvc) StreamInjectiveOraclePrices(ctx context.Context, m *derivativeExchangePB.DerivativeMarketInfo, marketInfo *DerivativeMarketInfo) { 251 | steam, err := s.oracleClient.StreamPrices(ctx, &oraclePB.StreamPricesRequest{ 252 | BaseSymbol: marketInfo.OracleBase, 253 | QuoteSymbol: marketInfo.OracleQuote, 254 | OracleType: marketInfo.OracleType.String(), 255 | }) 256 | if err != nil { 257 | message := fmt.Sprintf("Fail to connect oracle StreamPrices for %s with err: %s", m.Ticker, err.Error()) 258 | s.logger.Errorln(message) 259 | time.Sleep(time.Second * 10) 260 | return 261 | } 262 | s.logger.Infof("Connected to %s oracle StreamPrices", m.Ticker) 263 | for { 264 | select { 265 | case <-ctx.Done(): 266 | return 267 | default: 268 | resp, err := steam.Recv() 269 | if err != nil { 270 | message := fmt.Sprintf("Closing %s oracle StreamPrices with err: %s", m.Ticker, err.Error()) 271 | s.logger.Errorln(message) 272 | return 273 | } 274 | fmt.Println(resp.Price) 275 | } 276 | } 277 | } 278 | 279 | func (s *tradingSvc) StreamInjectiveDerivativeOrderBookSession(ctx context.Context, m *derivativeExchangePB.DerivativeMarketInfo, subaccountID *common.Hash, marketInfo *DerivativeMarketInfo) { 280 | for { 281 | select { 282 | case <-ctx.Done(): 283 | s.logger.Infof("Closing %s derivative orderbook session.", m.Ticker) 284 | return 285 | default: 286 | s.StreamInjectiveDerivativeOrderBook(ctx, m, subaccountID, marketInfo) 287 | message := fmt.Sprintf("Reconnecting %s derivative StreamOrderBook...", m.Ticker) 288 | s.logger.Errorln(message) 289 | } 290 | } 291 | } 292 | 293 | func (s *tradingSvc) StreamInjectiveDerivativeOrderBook(ctx context.Context, m *derivativeExchangePB.DerivativeMarketInfo, subaccountID *common.Hash, marketInfo *DerivativeMarketInfo) { 294 | steam, err := s.derivativesClient.StreamOrderbook(ctx, &derivativeExchangePB.StreamOrderbookRequest{ 295 | MarketIds: []string{m.MarketId}, 296 | }) 297 | if err != nil { 298 | message := fmt.Sprintf("Fail to connect derivative StreamOrderbook for %s with err: %s", m.Ticker, err.Error()) 299 | s.logger.Errorln(message) 300 | time.Sleep(time.Second * 10) 301 | return 302 | } 303 | s.logger.Infof("Connected to %s derivative StreamOrderbook", m.Ticker) 304 | for { 305 | select { 306 | case <-ctx.Done(): 307 | return 308 | default: 309 | resp, err := steam.Recv() 310 | if err != nil { 311 | message := fmt.Sprintf("Closing %s derivative StreamOrderbook with err: %s", m.Ticker, err.Error()) 312 | s.logger.Errorln(message) 313 | return 314 | } 315 | fmt.Println(resp) 316 | } 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /go-demo/trading/mm_strategy.go: -------------------------------------------------------------------------------- 1 | package trading 2 | 3 | import ( 4 | "context" 5 | "math" 6 | "time" 7 | 8 | derivativeExchangePB "github.com/InjectiveLabs/sdk-go/exchange/derivative_exchange_rpc/pb" 9 | oraclePB "github.com/InjectiveLabs/sdk-go/exchange/oracle_rpc/pb" 10 | cosmtypes "github.com/cosmos/cosmos-sdk/types" 11 | "github.com/shopspring/decimal" 12 | ) 13 | 14 | func (marketInfo *DerivativeMarketInfo) OraclePriceSession(ctx context.Context, idx int, s *tradingSvc, m *derivativeExchangePB.DerivativeMarketInfo) { 15 | var oracle decimal.Decimal 16 | marketInfo.OracleReady = false 17 | oracleCheck := time.NewTicker(time.Second * 5) 18 | defer oracleCheck.Stop() 19 | for { 20 | select { 21 | case <-ctx.Done(): 22 | return 23 | case <-oracleCheck.C: 24 | if resp, err := s.oracleClient.Price(ctx, &oraclePB.PriceRequest{ 25 | BaseSymbol: marketInfo.OracleBase, 26 | QuoteSymbol: marketInfo.OracleQuote, 27 | OracleType: marketInfo.OracleType.String(), 28 | }); err == nil { 29 | oracle, err = decimal.NewFromString(resp.Price) 30 | if err != nil { 31 | oracle = decimal.NewFromInt(0) 32 | continue 33 | } 34 | if !marketInfo.OracleReady { 35 | marketInfo.OracleReady = true 36 | } 37 | go s.RealTimeChecking(ctx, idx, m, marketInfo) 38 | marketInfo.OraclePrice = oracle 39 | marketInfo.CosmOraclePrice = getPriceForDerivative(marketInfo.OraclePrice, marketInfo.QuoteDenomDecimals, marketInfo.MinPriceTickSize) 40 | } 41 | default: 42 | time.Sleep(time.Second) 43 | } 44 | } 45 | } 46 | 47 | // basic as model for market making 48 | func (m *DerivativeMarketInfo) ReservationPriceAndSpread() { 49 | //r(s,t) = s - q * gamma * sigma**2 * (T-t) 50 | //spread[t] = gamma * (sigma **2) + (2/gamma) * math.log(1 + (gamma/k)) 51 | T := 1.0 // T-t set it 1 for 24hrs trading market, considering the risk more 52 | riskAversion := 0.01 53 | fillIndensity := 2.0 54 | dT := decimal.NewFromFloat(T) 55 | dgamma := decimal.NewFromFloat(riskAversion) 56 | dsigma := m.MarketVolatility 57 | sigma2 := dsigma.Pow(decimal.NewFromFloat(2)) 58 | m.ReservedPrice = m.OraclePrice.Sub(m.InventoryPCT.Mul(dgamma).Mul(sigma2).Mul(dT)) 59 | 60 | logpart := decimal.NewFromFloat(math.Log((1 + (riskAversion / fillIndensity)))) 61 | first := dgamma.Mul(sigma2) 62 | second := decimal.NewFromFloat(2).Div(dgamma) 63 | m.OptSpread = first.Add(second.Mul(logpart)) 64 | 65 | halfspread := m.OptSpread.Div(decimal.NewFromFloat(2)) 66 | m.BestAsk = m.ReservedPrice.Add(halfspread) 67 | m.BestBid = m.ReservedPrice.Sub(halfspread) 68 | } 69 | 70 | func (m *DerivativeMarketInfo) CalVolatility(scale decimal.Decimal) { 71 | m.MarketVolatility = decimal.NewFromFloat(0.01) 72 | } 73 | 74 | // set 100k as max position value 75 | func (m *DerivativeMarketInfo) CalInventoryPCT(positionMargin decimal.Decimal) { 76 | var scale decimal.Decimal 77 | switch m.PositionDirection { 78 | case "long": 79 | scale = decimal.NewFromInt(1) 80 | case "short": 81 | scale = decimal.NewFromInt(-1) 82 | } 83 | m.InventoryPCT = positionMargin.Mul(scale).Div(decimal.NewFromInt(100000)) 84 | } 85 | 86 | func (s *tradingSvc) RealTimeChecking(ctx context.Context, idx int, m *derivativeExchangePB.DerivativeMarketInfo, marketInfo *DerivativeMarketInfo) { 87 | position := s.GetInjectivePositionData(m.Ticker) 88 | switch { 89 | case position == nil: 90 | // no position 91 | marketInfo.PositionQty = decimal.NewFromInt(0) 92 | marketInfo.PositionDirection = "null" 93 | marketInfo.MarginValue = decimal.NewFromInt(0) 94 | default: 95 | marketInfo.PositionQty, _ = decimal.NewFromString(position.Quantity) 96 | scale := decimal.New(1, int32(-marketInfo.QuoteDenomDecimals)) 97 | marginCos, _ := decimal.NewFromString(position.Margin) 98 | marketInfo.MarginValue = marginCos.Mul(scale) 99 | marketInfo.PositionDirection = position.Direction 100 | } 101 | marketInfo.CalInventoryPCT(marketInfo.MarginValue) 102 | } 103 | 104 | func (s *tradingSvc) GetTopOfBook(ctx context.Context, m *derivativeExchangePB.DerivativeMarketInfo, marketInfo *DerivativeMarketInfo) error { 105 | book, err := s.derivativesClient.Orderbook(ctx, &derivativeExchangePB.OrderbookRequest{ 106 | MarketId: m.MarketId, 107 | }) 108 | if err != nil { 109 | return err 110 | } 111 | if len(book.Orderbook.Buys) > 0 { 112 | marketInfo.TopBidPrice = cosmtypes.MustNewDecFromStr(book.Orderbook.Buys[0].Price) 113 | } else { 114 | marketInfo.TopBidPrice = cosmtypes.NewDec(0) 115 | } 116 | if len(book.Orderbook.Sells) > 0 { 117 | marketInfo.TopAskPrice = cosmtypes.MustNewDecFromStr(book.Orderbook.Sells[0].Price) 118 | } else { 119 | marketInfo.TopAskPrice = cosmtypes.NewDec(0) 120 | } 121 | return nil 122 | } 123 | -------------------------------------------------------------------------------- /go-demo/trading/service.go: -------------------------------------------------------------------------------- 1 | package trading 2 | 3 | import ( 4 | "context" 5 | "runtime/debug" 6 | "strings" 7 | "sync" 8 | "time" 9 | 10 | cosmtypes "github.com/cosmos/cosmos-sdk/types" 11 | banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" 12 | "github.com/pkg/errors" 13 | "github.com/shopspring/decimal" 14 | log "github.com/sirupsen/logrus" 15 | 16 | chainclient "github.com/InjectiveLabs/sdk-go/chain/client" 17 | chaintypes "github.com/InjectiveLabs/sdk-go/chain/exchange/types" 18 | exchangetypes "github.com/InjectiveLabs/sdk-go/chain/exchange/types" 19 | accountsPB "github.com/InjectiveLabs/sdk-go/exchange/accounts_rpc/pb" 20 | derivativeExchangePB "github.com/InjectiveLabs/sdk-go/exchange/derivative_exchange_rpc/pb" 21 | exchangePB "github.com/InjectiveLabs/sdk-go/exchange/exchange_rpc/pb" 22 | oraclePB "github.com/InjectiveLabs/sdk-go/exchange/oracle_rpc/pb" 23 | spotExchangePB "github.com/InjectiveLabs/sdk-go/exchange/spot_exchange_rpc/pb" 24 | 25 | "go-bot-demo/metrics" 26 | ) 27 | 28 | const ( 29 | Reverse = "reverse" 30 | Same = "same" 31 | ) 32 | 33 | // sync other exchanges' market data 34 | type dataCenter struct { 35 | UpdateInterval time.Duration 36 | InjectiveSpotAccount InjectiveSpotAccountBranch 37 | InjectivePositions InjectivePositionsBranch 38 | InjectiveQuoteLen int 39 | InjectiveQuoteAssets []InjectiveQuoteAssetsBranch 40 | } 41 | 42 | type InjectiveQuoteAssetsBranch struct { 43 | Asset string 44 | Denom string 45 | QuoteDenomDecimals int 46 | AvailableBalance decimal.Decimal 47 | TotalBalance decimal.Decimal 48 | mux sync.RWMutex 49 | } 50 | 51 | type InjectiveSpotAccountBranch struct { 52 | res *accountsPB.SubaccountBalancesListResponse 53 | mux sync.RWMutex // read write lock 54 | Assets []string 55 | Denoms []string 56 | AvailableAmounts []decimal.Decimal 57 | TotalAmounts []decimal.Decimal 58 | TotalValues []decimal.Decimal 59 | LockedValues []decimal.Decimal 60 | } 61 | 62 | type InjectivePositionsBranch struct { 63 | res *derivativeExchangePB.PositionsResponse 64 | mux sync.RWMutex // read write lock 65 | } 66 | 67 | type Service interface { 68 | Start() error 69 | Close() 70 | } 71 | 72 | type tradingSvc struct { 73 | cosmosClient chainclient.CosmosClient 74 | exchangeQueryClient chaintypes.QueryClient 75 | bankQueryClient banktypes.QueryClient 76 | 77 | accountsClient accountsPB.InjectiveAccountsRPCClient 78 | exchangeClient exchangePB.InjectiveExchangeRPCClient 79 | spotsClient spotExchangePB.InjectiveSpotExchangeRPCClient 80 | derivativesClient derivativeExchangePB.InjectiveDerivativeExchangeRPCClient 81 | oracleClient oraclePB.InjectiveOracleRPCClient 82 | 83 | logger log.Logger 84 | svcTags metrics.Tags 85 | 86 | Cancel *context.CancelFunc 87 | 88 | injSymbols []string 89 | dataCenter dataCenter 90 | maxOrderValue []float64 91 | } 92 | 93 | func NewService( 94 | cosmosClient chainclient.CosmosClient, 95 | exchangeQueryClient chaintypes.QueryClient, 96 | bankQueryClient banktypes.QueryClient, 97 | accountsClient accountsPB.InjectiveAccountsRPCClient, 98 | exchangeClient exchangePB.InjectiveExchangeRPCClient, 99 | spotsClient spotExchangePB.InjectiveSpotExchangeRPCClient, 100 | derivativesClient derivativeExchangePB.InjectiveDerivativeExchangeRPCClient, 101 | oracleClient oraclePB.InjectiveOracleRPCClient, 102 | injSymbols []string, 103 | maxOrderValue []float64, 104 | ) Service { 105 | // set log time stamp formatter 106 | formatter := &log.TextFormatter{ 107 | FullTimestamp: true, 108 | TimestampFormat: "2006-01-02 15:04:05.999", 109 | } 110 | logger := log.New() 111 | logger.SetFormatter(formatter) 112 | logger.WithField("svc", "trading") 113 | return &tradingSvc{ 114 | logger: *logger, 115 | svcTags: metrics.Tags{ 116 | "svc": "sgmm_bot", 117 | }, 118 | 119 | cosmosClient: cosmosClient, 120 | exchangeQueryClient: exchangeQueryClient, 121 | bankQueryClient: bankQueryClient, 122 | accountsClient: accountsClient, 123 | exchangeClient: exchangeClient, 124 | spotsClient: spotsClient, 125 | derivativesClient: derivativesClient, 126 | oracleClient: oracleClient, 127 | injSymbols: injSymbols, 128 | maxOrderValue: maxOrderValue, 129 | } 130 | } 131 | 132 | func (s *tradingSvc) DepositAllBankBalances(ctx context.Context) { 133 | sender := s.cosmosClient.FromAddress() 134 | resp, err := s.bankQueryClient.AllBalances(ctx, &banktypes.QueryAllBalancesRequest{ 135 | Address: sender.String(), 136 | Pagination: nil, 137 | }) 138 | 139 | if err != nil { 140 | panic("Need bank balances") 141 | } 142 | msgs := make([]cosmtypes.Msg, 0) 143 | subaccountID := defaultSubaccount(sender) 144 | 145 | s.logger.Infoln("Preparing Injective Chain funds for deposit into exchange subaccount.") 146 | 147 | for _, balance := range resp.Balances { 148 | if balance.IsZero() { 149 | continue 150 | } 151 | 152 | // never let INJ balance go under 100 153 | if balance.Denom == "inj" { 154 | minINJAmount, _ := cosmtypes.NewIntFromString("200000000000000000000") 155 | if balance.Amount.LT(minINJAmount) { 156 | continue 157 | } else { 158 | balance.Amount = balance.Amount.Sub(minINJAmount) 159 | } 160 | } 161 | 162 | s.logger.Infof("%s:\t%s \t %s\n", balance.Denom, balance.Amount.String(), subaccountID.Hex()) 163 | msg := exchangetypes.MsgDeposit{ 164 | Sender: sender.String(), 165 | SubaccountId: subaccountID.Hex(), 166 | Amount: balance, 167 | } 168 | msgs = append(msgs, &msg) 169 | } 170 | if len(msgs) > 0 { 171 | if _, err := s.cosmosClient.SyncBroadcastMsg(msgs...); err != nil { 172 | s.logger.Errorf("Failed depositing to exchange with error %s", err.Error()) 173 | } 174 | } 175 | } 176 | 177 | func (s *tradingSvc) MarketMakeDerivativeMarkets(ctx context.Context) { 178 | s.dataCenter.UpdateInterval = 2000 // milli sec 179 | // setting strategy 180 | quotes := []string{"USDT"} 181 | accountCheckInterval := time.Duration(20) 182 | 183 | // get account balances data first 184 | if err := s.GetInjectiveSpotAccount(ctx); err != nil { 185 | s.logger.Infof("Failed to get Injective account") 186 | return 187 | } 188 | resp, err := s.spotsClient.Markets(ctx, &spotExchangePB.MarketsRequest{}) 189 | if err != nil || resp == nil { 190 | s.logger.Infof("Failed to get spot markets") 191 | return 192 | } 193 | 194 | s.InitialInjectiveAccountBalances(quotes, resp) 195 | s.InitialInjAssetWithDenom(quotes, resp) 196 | 197 | go s.UpdateInjectiveSpotAccountSession(ctx, accountCheckInterval) 198 | 199 | if err := s.GetInjectivePositions(ctx); err != nil { 200 | s.logger.Infof("Failed to get Injective positions") 201 | return 202 | } 203 | go s.UpdateInjectivePositionSession(ctx, accountCheckInterval) 204 | 205 | respDerivative, err := s.derivativesClient.Markets(ctx, &derivativeExchangePB.MarketsRequest{}) 206 | if err != nil || respDerivative == nil { 207 | s.logger.Infof("Failed to get derivative markets") 208 | return 209 | } 210 | 211 | var strategyCount int = 0 212 | for _, market := range respDerivative.Markets { 213 | var adjTicker string 214 | if strings.Contains(market.Ticker, " ") { 215 | tmp := strings.Split(market.Ticker, " ") 216 | if tmp[1] != "PERP" || len(tmp) != 2 { 217 | continue 218 | } 219 | adjTicker = tmp[0] 220 | } 221 | for i, symbol := range s.injSymbols { 222 | if symbol == adjTicker { 223 | go s.SingleExchangeMM(ctx, market, i, accountCheckInterval) 224 | strategyCount++ 225 | } 226 | } 227 | } 228 | s.logger.Infof("Launching %d strategy!", strategyCount) 229 | } 230 | 231 | func (s *tradingSvc) Start() (err error) { 232 | defer s.panicRecover(&err) 233 | // main bot loop 234 | ctx, cancel := context.WithCancel(context.Background()) 235 | s.Cancel = &cancel 236 | s.DepositAllBankBalances(ctx) 237 | go s.MarketMakeDerivativeMarkets(ctx) 238 | return nil 239 | } 240 | 241 | func (s *tradingSvc) panicRecover(err *error) { 242 | if r := recover(); r != nil { 243 | *err = errors.Errorf("%v", r) 244 | 245 | if e, ok := r.(error); ok { 246 | s.logger.WithError(e).Errorln("service main loop panicked with an error") 247 | s.logger.Debugln(string(debug.Stack())) 248 | } else { 249 | s.logger.Errorln(r) 250 | } 251 | } 252 | } 253 | 254 | func (s *tradingSvc) Close() { 255 | // graceful shutdown if needed 256 | (*s.Cancel)() 257 | } 258 | -------------------------------------------------------------------------------- /go-demo/trading/singleExchangeMM.go: -------------------------------------------------------------------------------- 1 | package trading 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "strconv" 8 | "strings" 9 | "sync" 10 | "time" 11 | 12 | exchangetypes "github.com/InjectiveLabs/sdk-go/chain/exchange/types" 13 | derivativeExchangePB "github.com/InjectiveLabs/sdk-go/exchange/derivative_exchange_rpc/pb" 14 | cosmtypes "github.com/cosmos/cosmos-sdk/types" 15 | "github.com/shopspring/decimal" 16 | "github.com/tendermint/tendermint/libs/rand" 17 | ) 18 | 19 | func (s *tradingSvc) SingleExchangeMM(ctx context.Context, m *derivativeExchangePB.DerivativeMarketInfo, idx int, interval time.Duration) { 20 | s.logger.Infof("✅ Start strategy for %s derivative market.", m.Ticker) 21 | var Initial bool = true 22 | var wg sync.WaitGroup 23 | var printOrdersTime time.Time 24 | 25 | sender := s.cosmosClient.FromAddress() 26 | subaccountID := defaultSubaccount(sender) 27 | marketInfo := DerivativeMarketToMarketInfo(m) 28 | // set max order value from .env 29 | marketInfo.MaxOrderValue = decimal.NewFromInt(int64(s.maxOrderValue[idx])) 30 | 31 | buffTickLevel := marketInfo.MinPriceTickSize.Mul(cosmtypes.NewDec(int64(20))) 32 | 33 | marketInfo.LastSendMessageTime = make([]time.Time, 4) // depends on how many critical alerts wanna send with 10 min interval 34 | // inj stream trades session 35 | go s.StreamInjectiveDerivativeTradeSession(ctx, m, &subaccountID, marketInfo) 36 | // inj stream orders session 37 | //go s.StreamInjectiveDerivativeOrderSession(ctx, m, &subaccountID, marketInfo, ErrCh) 38 | // inj stream position session 39 | go s.StreamInjectiveDerivativePositionSession(ctx, m, &subaccountID, marketInfo) 40 | // inital orders snapshot first 41 | //s.InitialOrderList(ctx, m, &subaccountID, marketInfo, ErrCh) 42 | // oracle price session 43 | go marketInfo.OraclePriceSession(ctx, idx, s, m) 44 | time.Sleep(time.Second * 5) 45 | for { 46 | select { 47 | case <-ctx.Done(): 48 | return 49 | default: 50 | // check oracle price part 51 | if !marketInfo.OracleReady { 52 | message := fmt.Sprintf("Wait 5 sec for connecting reference price") 53 | s.logger.Errorln(message) 54 | time.Sleep(time.Second * 5) 55 | continue 56 | } 57 | // initial orders 58 | s.InitialOrderList(ctx, m, &subaccountID, marketInfo) 59 | 60 | scale := decimal.NewFromInt(1) 61 | // strategy part, need to calculate the staking window. 62 | marketInfo.CalVolatility(scale) 63 | marketInfo.ReservationPriceAndSpread() 64 | bestAskPrice := getPriceForDerivative(marketInfo.BestAsk, marketInfo.QuoteDenomDecimals, marketInfo.MinPriceTickSize) 65 | bestBidPrice := getPriceForDerivative(marketInfo.BestBid, marketInfo.QuoteDenomDecimals, marketInfo.MinPriceTickSize) 66 | 67 | bidLen := marketInfo.GetBidOrdersLen() 68 | askLen := marketInfo.GetAskOrdersLen() 69 | 70 | if time.Now().After(printOrdersTime.Add(time.Second*5)) && marketInfo.IsOrderUpdated() { 71 | var buffer bytes.Buffer 72 | buffer.WriteString(marketInfo.Ticker) 73 | buffer.WriteString(" having ") 74 | buffer.WriteString(strconv.Itoa(bidLen)) 75 | buffer.WriteString(" BUY orders, ") 76 | buffer.WriteString(strconv.Itoa(askLen)) 77 | buffer.WriteString(" SELL orders\n") 78 | space := strings.Repeat(" ", 30) 79 | buffer.WriteString(space) 80 | buffer.WriteString("reservated diff: ") 81 | diff := marketInfo.ReservedPrice.Sub(marketInfo.OraclePrice).Round(2) 82 | buffer.WriteString(diff.String()) 83 | buffer.WriteString(", spread: ") 84 | spread := marketInfo.BestAsk.Sub(marketInfo.BestBid).Div(marketInfo.BestBid).Mul(decimal.NewFromInt(100)).Round(4) 85 | buffer.WriteString(spread.String()) 86 | buffer.WriteString("%,\n") 87 | buffer.WriteString(space) 88 | switch marketInfo.PositionDirection { 89 | case "null": 90 | buffer.WriteString("no position.") 91 | default: 92 | buffer.WriteString(marketInfo.PositionDirection) 93 | buffer.WriteString(" ") 94 | buffer.WriteString(marketInfo.PositionQty.String()) 95 | buffer.WriteString("(") 96 | buffer.WriteString(marketInfo.MarginValue.Round(2).String()) 97 | buffer.WriteString("USD)") 98 | } 99 | s.logger.Infoln(buffer.String()) 100 | printOrdersTime = time.Now() 101 | } 102 | 103 | // best limit order part 104 | bidOrdersmsgs := make([]exchangetypes.DerivativeOrder, 0, 1) 105 | askOrdersmsgs := make([]exchangetypes.DerivativeOrder, 0, 1) 106 | oldBidOrder := marketInfo.GetBidOrder(0) 107 | oldAskOrder := marketInfo.GetAskOrder(0) 108 | 109 | // cancel ladder limit order part 110 | bidCancelmsgs := make([]exchangetypes.OrderData, 0, 10) 111 | askCancelmsgs := make([]exchangetypes.OrderData, 0, 10) 112 | 113 | if bidLen > 0 { 114 | wg.Add(1) 115 | go func() { 116 | defer wg.Done() 117 | var canceledOrders []*derivativeExchangePB.DerivativeLimitOrder 118 | marketInfo.BidOrders.mux.RLock() 119 | for _, order := range marketInfo.BidOrders.Orders { // if the order is out of the window, cancel it 120 | orderPrice := cosmtypes.MustNewDecFromStr(order.Price) 121 | if orderPrice.LT(bestBidPrice.Sub(buffTickLevel)) || orderPrice.Add(marketInfo.MinPriceTickSize).GT(bestBidPrice) || orderPrice.Add(marketInfo.MinPriceTickSize).GTE(bestAskPrice) { 122 | data := exchangetypes.OrderData{ 123 | MarketId: m.MarketId, 124 | SubaccountId: subaccountID.Hex(), 125 | OrderHash: order.OrderHash, 126 | } 127 | bidCancelmsgs = append(bidCancelmsgs, data) 128 | canceledOrders = append(canceledOrders, order) 129 | } 130 | } 131 | marketInfo.BidOrders.mux.RUnlock() 132 | marketInfo.CancelBidOrdersFromOrderList(canceledOrders) 133 | }() 134 | } 135 | if askLen > 0 { 136 | wg.Add(1) 137 | go func() { 138 | defer wg.Done() 139 | var canceledOrders []*derivativeExchangePB.DerivativeLimitOrder 140 | marketInfo.AskOrders.mux.Lock() 141 | for _, order := range marketInfo.AskOrders.Orders { 142 | orderPrice := cosmtypes.MustNewDecFromStr(order.Price) 143 | if orderPrice.GT(bestAskPrice.Add(buffTickLevel)) && orderPrice.Sub(marketInfo.MinPriceTickSize).LT(bestAskPrice) || orderPrice.Sub(marketInfo.MinPriceTickSize).LTE(bestBidPrice) { 144 | data := exchangetypes.OrderData{ 145 | MarketId: m.MarketId, 146 | SubaccountId: subaccountID.Hex(), 147 | OrderHash: order.OrderHash, 148 | } 149 | askCancelmsgs = append(askCancelmsgs, data) 150 | canceledOrders = append(canceledOrders, order) 151 | } 152 | } 153 | marketInfo.AskOrders.mux.Unlock() 154 | marketInfo.CancelAskOrdersFromOrderList(canceledOrders) 155 | }() 156 | } 157 | wg.Wait() 158 | 159 | // cancel orders part 160 | cancelOrderMsgs := &exchangetypes.MsgBatchCancelDerivativeOrders{ 161 | Sender: sender.String(), 162 | } 163 | var allCanceledOrders int 164 | bidCancelLen := len(bidCancelmsgs) 165 | if bidCancelLen != 0 { 166 | cancelOrderMsgs.Data = append(cancelOrderMsgs.Data, bidCancelmsgs...) 167 | allCanceledOrders += bidCancelLen 168 | } 169 | askCancelLen := len(askCancelmsgs) 170 | if askCancelLen != 0 { 171 | cancelOrderMsgs.Data = append(cancelOrderMsgs.Data, askCancelmsgs...) 172 | allCanceledOrders += askCancelLen 173 | 174 | } 175 | 176 | if allCanceledOrders != 0 { 177 | if !marketInfo.IsOrderUpdated() { 178 | if Initial { 179 | Initial = false 180 | } 181 | time.Sleep(time.Millisecond * s.dataCenter.UpdateInterval) 182 | continue 183 | } 184 | s.HandleRequestMsgs( 185 | "cancel", 186 | allCanceledOrders, 187 | cancelOrderMsgs, 188 | marketInfo, 189 | ) 190 | // cancel first then place orders 191 | if Initial { 192 | Initial = false 193 | } 194 | time.Sleep(time.Millisecond * s.dataCenter.UpdateInterval) 195 | continue 196 | } 197 | 198 | if err := s.GetTopOfBook(ctx, m, marketInfo); err != nil { 199 | message := fmt.Sprintf("Wait 5 sec for getting injective orderbook") 200 | s.logger.Errorln(message) 201 | time.Sleep(time.Second * 5) 202 | continue 203 | } 204 | 205 | // limit order part 206 | // decide max order size in coin 207 | marketInfo.MaxOrderSize = marketInfo.MaxOrderValue.Div(marketInfo.OraclePrice) 208 | 209 | wg.Add(2) 210 | go func() { 211 | defer wg.Done() 212 | // best bid 213 | var Updating bool = false 214 | bidLen = marketInfo.GetBidOrdersLen() 215 | switch { 216 | case bidLen == 0: 217 | Updating = true 218 | case bidLen == 2: 219 | return 220 | default: 221 | oldBestPrice := cosmtypes.MustNewDecFromStr(oldBidOrder.Price) 222 | if bestBidPrice.GT(oldBestPrice.Add(buffTickLevel)) || bestBidPrice.LT(oldBestPrice) { 223 | Updating = true 224 | } 225 | } 226 | randNumQ := decimal.NewFromInt(int64(rand.Intn(40))).Add(decimal.NewFromInt(60)).Div(decimal.NewFromInt(100)) // 0.6~1 227 | orderSize, placeOrder := s.getCorrectOrderSize(marketInfo, "buy", marketInfo.MaxOrderSize.Mul(randNumQ)) 228 | if Updating && placeOrder { 229 | price := bestBidPrice 230 | uplimit := bestAskPrice 231 | if marketInfo.CheckNewOrderIsSafe("buy", price, uplimit, bestBidPrice, oldBidOrder, oldAskOrder) { 232 | if !marketInfo.TopAskPrice.IsZero() && price.GT(marketInfo.TopAskPrice) { 233 | price = marketInfo.TopAskPrice 234 | } 235 | order := NewDerivativeOrder(subaccountID, marketInfo, &DerivativeOrderData{ 236 | OrderType: 1, 237 | Price: price, 238 | Quantity: orderSize, 239 | Margin: price.Mul(orderSize).Quo(cosmtypes.NewDec(int64(2))), 240 | FeeRecipient: sender.String(), 241 | }) 242 | bidOrdersmsgs = append(bidOrdersmsgs, *order) 243 | bidLen++ 244 | } 245 | } 246 | }() 247 | go func() { 248 | defer wg.Done() 249 | // best ask 250 | var Updating bool = false 251 | askLen = marketInfo.GetAskOrdersLen() 252 | switch { 253 | case askLen == 0: 254 | Updating = true 255 | case askLen == 2: 256 | return 257 | default: 258 | oldBestPrice := cosmtypes.MustNewDecFromStr(oldAskOrder.Price) 259 | if bestAskPrice.GT(oldBestPrice) || bestAskPrice.LT(oldBestPrice.Sub(buffTickLevel)) { 260 | Updating = true 261 | } 262 | } 263 | randNumQ := decimal.NewFromInt(int64(rand.Intn(40))).Add(decimal.NewFromInt(60)).Div(decimal.NewFromInt(100)) // 0.6~1 264 | orderSize, placeOrder := s.getCorrectOrderSize(marketInfo, "sell", marketInfo.MaxOrderSize.Mul(randNumQ)) 265 | if Updating && placeOrder { 266 | price := bestAskPrice 267 | lowlimit := bestBidPrice 268 | if marketInfo.CheckNewOrderIsSafe("sell", price, bestAskPrice, lowlimit, oldBidOrder, oldAskOrder) { 269 | if !marketInfo.TopBidPrice.IsZero() && price.LT(marketInfo.TopBidPrice) { 270 | price = marketInfo.TopBidPrice 271 | } 272 | order := NewDerivativeOrder(subaccountID, marketInfo, &DerivativeOrderData{ 273 | OrderType: 2, 274 | Price: price, 275 | Quantity: orderSize, 276 | Margin: price.Mul(orderSize).Quo(cosmtypes.NewDec(int64(2))), 277 | FeeRecipient: sender.String(), 278 | }) 279 | askOrdersmsgs = append(askOrdersmsgs, *order) 280 | askLen++ 281 | } 282 | } 283 | }() 284 | wg.Wait() 285 | 286 | // place orders part 287 | ordermsgs := &exchangetypes.MsgBatchCreateDerivativeLimitOrders{ 288 | Sender: sender.String(), 289 | } 290 | var allOrdersLen int 291 | bidOrdersLen := len(bidOrdersmsgs) 292 | if bidOrdersLen != 0 { 293 | ordermsgs.Orders = append(ordermsgs.Orders, bidOrdersmsgs...) 294 | allOrdersLen += bidOrdersLen 295 | } 296 | askOrdersLen := len(askOrdersmsgs) 297 | if askOrdersLen != 0 { 298 | ordermsgs.Orders = append(ordermsgs.Orders, askOrdersmsgs...) 299 | allOrdersLen += askOrdersLen 300 | } 301 | if allOrdersLen != 0 { 302 | if !marketInfo.IsOrderUpdated() { 303 | if Initial { 304 | Initial = false 305 | } 306 | time.Sleep(time.Millisecond * s.dataCenter.UpdateInterval) 307 | continue 308 | } 309 | s.HandleRequestMsgs( 310 | "post", 311 | allOrdersLen, 312 | ordermsgs, 313 | marketInfo, 314 | ) 315 | } 316 | 317 | if Initial { 318 | Initial = false 319 | } 320 | time.Sleep(time.Millisecond * s.dataCenter.UpdateInterval) 321 | } 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /go-demo/trading/singleExchangeMMparts.go: -------------------------------------------------------------------------------- 1 | package trading 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | exchangetypes "github.com/InjectiveLabs/sdk-go/chain/exchange/types" 11 | cosmtypes "github.com/cosmos/cosmos-sdk/types" 12 | "github.com/shopspring/decimal" 13 | ) 14 | 15 | // check the balance before placing order 16 | func (s *tradingSvc) getCorrectOrderSize(m *DerivativeMarketInfo, method string, qty decimal.Decimal) (formatedQty cosmtypes.Dec, enough bool) { 17 | formatedQty = cosmtypes.NewDec(0) 18 | enough = true 19 | if enough { 20 | formatedQty = getQuantityForDerivative(qty, m.MinQuantityTickSize, m.QuoteDenomDecimals) 21 | } 22 | return formatedQty, enough 23 | } 24 | 25 | func (s *tradingSvc) HandleRequestMsgs( 26 | method string, 27 | allRequests int, 28 | Msgs interface{}, 29 | marketInfo *DerivativeMarketInfo, 30 | ) { 31 | if allRequests == 0 { 32 | return 33 | } 34 | var batch bool = true 35 | switch method { 36 | case "cancel": 37 | msgs := Msgs.(*exchangetypes.MsgBatchCancelDerivativeOrders) 38 | CosMsgs := []cosmtypes.Msg{msgs} 39 | for { 40 | if allRequests == 0 { 41 | return 42 | } 43 | if res, err := s.cosmosClient.AsyncBroadcastMsg(CosMsgs...); err != nil { 44 | if strings.Contains(err.Error(), "order doesnt exist") { 45 | if batch { 46 | cancelMsgs := make([]cosmtypes.Msg, 0, 10) 47 | for _, item := range msgs.Data { 48 | msg := &exchangetypes.MsgCancelDerivativeOrder{ 49 | Sender: msgs.Sender, 50 | MarketId: item.MarketId, 51 | SubaccountId: item.SubaccountId, 52 | OrderHash: item.OrderHash, 53 | } 54 | cancelMsgs = append(cancelMsgs, msg) 55 | } 56 | CosMsgs = cancelMsgs 57 | allRequests = len(CosMsgs) 58 | batch = false 59 | } else { 60 | tmp := strings.Split(err.Error(), ":") 61 | idxStr := strings.Replace(tmp[3], " ", "", -1) 62 | idx, err := strconv.Atoi(idxStr) 63 | if err != nil { 64 | // handle 65 | } 66 | // remove error msg from msg list 67 | // debugging 68 | fmt.Println(CosMsgs[idx]) 69 | CosMsgs = append(CosMsgs[:idx], CosMsgs[idx+1:]...) 70 | allRequests = len(CosMsgs) 71 | } 72 | continue 73 | } 74 | messageIdx := 2 75 | if time.Now().After(marketInfo.LastSendMessageTime[messageIdx].Add(time.Second * 600)) { 76 | var buffer bytes.Buffer 77 | buffer.WriteString(marketInfo.Ticker) 78 | buffer.WriteString(" derivative market: ") 79 | if allRequests != 0 { 80 | resStr := "" 81 | if res != nil { 82 | resStr = res.String() 83 | } 84 | message := fmt.Sprintf("Fail to %s %d orders (%s)", method, allRequests, resStr) 85 | buffer.WriteString(message) 86 | } 87 | buffer.WriteString("with err: ") 88 | buffer.WriteString(err.Error()) 89 | buffer.WriteString("\n") 90 | message := buffer.String() 91 | s.logger.Errorln(message) 92 | marketInfo.LastSendMessageTime[messageIdx] = time.Now() 93 | } 94 | } else { 95 | if allRequests != 0 { 96 | var buffer bytes.Buffer 97 | buffer.WriteString(marketInfo.Ticker) 98 | buffer.WriteString(" : ") 99 | message := fmt.Sprintf("🗑 canceled %v ", allRequests) 100 | buffer.WriteString(message) 101 | if batch { 102 | buffer.WriteString("orders with batch") 103 | } else { 104 | buffer.WriteString("orders") 105 | } 106 | s.logger.Infoln(buffer.String()) 107 | // update order status 108 | marketInfo.UpdateOrderMain(Reduce) 109 | } 110 | return 111 | } 112 | } 113 | case "post": 114 | msgs := Msgs.(*exchangetypes.MsgBatchCreateDerivativeLimitOrders) 115 | CosMsgs := []cosmtypes.Msg{msgs} 116 | if res, err := s.cosmosClient.AsyncBroadcastMsg(CosMsgs...); err != nil { 117 | messageIdx := 2 118 | if time.Now().After(marketInfo.LastSendMessageTime[messageIdx].Add(time.Second * 600)) { 119 | var buffer bytes.Buffer 120 | buffer.WriteString(marketInfo.Ticker) 121 | buffer.WriteString(" derivative market: ") 122 | if allRequests != 0 { 123 | resStr := "" 124 | if res != nil { 125 | resStr = res.String() 126 | } 127 | message := fmt.Sprintf("Fail to %s %d orders (%s)", method, allRequests, resStr) 128 | buffer.WriteString(message) 129 | } 130 | buffer.WriteString("with err: ") 131 | buffer.WriteString(err.Error()) 132 | buffer.WriteString("\n") 133 | message := buffer.String() 134 | s.logger.Errorln(message) 135 | marketInfo.LastSendMessageTime[messageIdx] = time.Now() 136 | } 137 | } else { 138 | if allRequests != 0 { 139 | var buffer bytes.Buffer 140 | buffer.WriteString(marketInfo.Ticker) 141 | buffer.WriteString(" : ") 142 | message := fmt.Sprintf("🔥 posted %v ", allRequests) 143 | buffer.WriteString(message) 144 | buffer.WriteString("orders with batch") 145 | s.logger.Infoln(buffer.String()) 146 | // update order status 147 | marketInfo.UpdateOrderMain(Add) 148 | } 149 | } 150 | case "cut": 151 | msgs := Msgs.(*exchangetypes.MsgCreateDerivativeMarketOrder) 152 | CosMsgs := []cosmtypes.Msg{msgs} 153 | if res, err := s.cosmosClient.AsyncBroadcastMsg(CosMsgs...); err != nil { 154 | messageIdx := 2 155 | if time.Now().After(marketInfo.LastSendMessageTime[messageIdx].Add(time.Second * 600)) { 156 | var buffer bytes.Buffer 157 | buffer.WriteString(marketInfo.Ticker) 158 | buffer.WriteString(" derivative market: ") 159 | if allRequests != 0 { 160 | resStr := "" 161 | if res != nil { 162 | resStr = res.String() 163 | } 164 | message := fmt.Sprintf("Fail to %s %d orders (%s)", method, allRequests, resStr) 165 | buffer.WriteString(message) 166 | } 167 | buffer.WriteString("with err: ") 168 | buffer.WriteString(err.Error()) 169 | buffer.WriteString("\n") 170 | message := buffer.String() 171 | s.logger.Errorln(message) 172 | marketInfo.LastSendMessageTime[messageIdx] = time.Now() 173 | } 174 | } else { 175 | if allRequests != 0 { 176 | var buffer bytes.Buffer 177 | buffer.WriteString(marketInfo.Ticker) 178 | buffer.WriteString(" : ") 179 | message := fmt.Sprintf("🔥 cutted %v ", allRequests) 180 | buffer.WriteString(message) 181 | buffer.WriteString("orders with batch") 182 | s.logger.Infoln(buffer.String()) 183 | // update order status 184 | marketInfo.UpdateOrderMain(Reduce) 185 | } 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /go-demo/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | ) 7 | 8 | var ( 9 | AppVersion = "" 10 | GitCommit = "" 11 | BuildDate = "" 12 | 13 | GoVersion = "" 14 | GoArch = "" 15 | ) 16 | 17 | func init() { 18 | if len(AppVersion) == 0 { 19 | AppVersion = "dev" 20 | } 21 | 22 | GoVersion = runtime.Version() 23 | GoArch = runtime.GOARCH 24 | } 25 | 26 | func Version() string { 27 | return fmt.Sprintf( 28 | "Version %s (%s)\nCompiled at %s using Go %s (%s)", 29 | AppVersion, 30 | GitCommit, 31 | BuildDate, 32 | GoVersion, 33 | GoArch, 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /logos/Logo_stacked_Brand_Black_with_space.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InjectiveLabs/injective-api-demo/7bb5bfc0f8bcff2ee1cb304e31a545bf067e5b20/logos/Logo_stacked_Brand_Black_with_space.png -------------------------------------------------------------------------------- /python-demo/README.md: -------------------------------------------------------------------------------- 1 | ## Prerequisites 2 | 3 | python 3.7+ 4 | 5 | pyinjective (please install latest code in master branch from github, https://github.com/InjectiveLabs/sdk-python) 6 | 7 | ### Install injective python_sdk package 8 | 9 | ```bash 10 | pip install injective-py 11 | ``` 12 | 13 | If you had problems while installing the injective python_sdk package, you should install the dependencies in 14 | https://ofek.dev/coincurve/ 15 | 16 | You could find more information about injective-py in https://pypi.org/project/injective-py/ 17 | 18 | If the latest package is not uploaded to pypi, you use the following commands to update `injective-py` 19 | 20 | ```bash 21 | git clone https://github.com/InjectiveLabs/sdk-python.git 22 | python setup.py install 23 | ``` 24 | ## Decimal 25 | 26 | One thing you may need to pay more attention to is how to deal with decimals in injective exchange. As we all known, different crypto currecies require diffrent decimal precisions. Separately, ERC-20 tokens (e.g. INJ) have decimals of 18 or another number (like 6 for USDT and USDC). So on Injective, that means **having 1 INJ is 1e18 inj** and that **1 USDT is actually 1000000 peggy0xdac17f958d2ee523a2206206994597c13d831ec7**. 27 | 28 | For spot markets, a price reflects the **relative exchange rate** between two tokens. If the tokens have the same decimal scale, that's great since the prices become interpretable e.g. USDT/USDC (both have 6 decimals e.g. for USDT https://etherscan.io/address/0xdac17f958d2ee523a2206206994597c13d831ec7#readContract) or MATIC/INJ (both have 18 decimals) since the decimals cancel out. Prices however start to look wonky once you have exchanges between two tokens of different decimals, which unfortunately is most pairs with USDT or USDC denominations. As such, injective-py has simple utility functions that maintain a hardcoded dictionary for conversions and you can also achieve such utilities by yourself (e.g. you can use external API like Alchemy's getTokenMetadata to fetch decimal of base and quote asset). 29 | 30 | So for INJ/USDT of 6.9, the price you end up getting is 6.9*10^(6 - 18) = 6.9e-12. Note that this market also happens to have a MinPriceTickSize of 1e-15. This makes sense since since it's defining the minimum price increment of the relative exchange of INJ to USDT. Note that this market also happens to have a MinQuantityTickSize of 1e15. This also makes sense since it refers to the minimum INJ quantity tick size each order must have, which is 1e15/1e18 = 0.001 INJ. 31 | 32 | ## Suggestions 33 | 34 | Feel free to create an issue or contact API support if you have any errors. 35 | 36 | A few suggestions on reporting demo or API issues. 37 | 38 | 1. Before creating any issue, please make sure it is not a duplicate of an existing issue 39 | 2. Open an issue on this repo and label these issues properly with (bugs, enhancement, features, etc), and mentioned `python_demo` in title. 40 | 3. For each issue, please explain the issue, how to reproduce it, and present enough proofs (logs, screen shots, raw responses, etc) 41 | 4. Going the extra mile is appreciated when reporting any issues as it makes resolving the issue much easier and more efficient. 42 | 43 | -------------------------------------------------------------------------------- /python-demo/core/templates/market_making_template.py: -------------------------------------------------------------------------------- 1 | from asyncio import ( 2 | Event, 3 | ) 4 | import logging 5 | from pyinjective.wallet import PrivateKey 6 | 7 | from sortedcontainers import SortedList 8 | from core.object import ( 9 | PositionDerivative, 10 | MarketDerivative, 11 | BalanceDerivative, 12 | ) 13 | from configparser import SectionProxy 14 | from typing import List, Dict 15 | from util.misc import ( 16 | build_client_and_composer, 17 | switch_network, 18 | ) 19 | 20 | 21 | class MarketMaker: 22 | def __init__( 23 | self, 24 | configs: SectionProxy, 25 | ): 26 | priv_key = configs["private_key"] 27 | nodes = configs["nodes"].split(",") 28 | self.nodes = nodes 29 | self.is_mainnet = configs.getboolean("is_mainnet", False) 30 | self.node_index, self.network, insecure = switch_network( 31 | self.nodes, 0, self.is_mainnet 32 | ) 33 | self.client, self.composer = build_client_and_composer(self.network, insecure) 34 | 35 | # load account 36 | self.priv_key = PrivateKey.from_hex(priv_key) 37 | self.pub_key = self.priv_key.to_public_key() 38 | self.address = self.pub_key.to_address().init_num_seq(self.network.lcd_endpoint) 39 | self.subaccount_id = self.address.get_subaccount_id(index=0) 40 | logging.info("Subaccount ID: %s" % self.subaccount_id) 41 | 42 | self.fee_recipient = "inj1wrg096y69grgf8yg6tqxnh0tdwx4x47rsj8rs3" 43 | self.update_interval = configs.getint("update_interval", 60) 44 | logging.info("update interval: %d" % self.update_interval) 45 | 46 | ############################ 47 | self.position = PositionDerivative() 48 | 49 | if configs.get("base") and configs.get("quote"): 50 | self.market = MarketDerivative( 51 | base=configs["base"], 52 | quote=configs["quote"], 53 | network=self.network, 54 | is_mainnet=self.is_mainnet, 55 | ) 56 | else: 57 | raise Exception("invalid base or quote ticker") 58 | self.balance = BalanceDerivative( 59 | available_balance=configs.getfloat("available_balance", 10.0) 60 | ) 61 | 62 | self.ask_price = 0 63 | self.bid_price = 0 64 | 65 | self.n_orders = configs.getint("n_orders", 5) 66 | logging.info("n_orders: %s" % self.n_orders) 67 | self.leverage = configs.getfloat("leverage", 1) 68 | logging.info("leverage: %s" % self.leverage) 69 | 70 | # Avellaneda Stoikov model parameters 71 | 72 | self.orders: Dict[str, SortedList] = { 73 | "bids": SortedList(), 74 | "asks": SortedList(), 75 | "reduce_only_orders": SortedList(), 76 | } 77 | 78 | self.tob_bid_price = 0.0 79 | self.sob_best_bid_price = 0.0 80 | self.tob_ask_price = 0.0 81 | self.sob_best_ask_price = 0.0 82 | 83 | self.last_order_delta = configs.getfloat("last_order_delta", 0.02) 84 | self.first_order_delta = configs.getfloat("first_order_delta", 0.00) 85 | logging.info( 86 | "first_order_delta: %s, last_order_delta: %s :" 87 | % (self.first_order_delta, self.last_order_delta) 88 | ) 89 | 90 | self.first_asset_allocation = configs.getfloat("first_asset_allocation", 0) 91 | self.last_asset_allocation = configs.getfloat("last_asset_allocation", 0.4) 92 | 93 | self.estimated_fee_ratio = configs.getfloat("estimated_fee_ratio", 0.005) 94 | self.initial_margin_ratio = 1 95 | 96 | self.bid_total_asset_allocation = configs.getfloat( 97 | "bid_total_asset_allocation", 0.09 98 | ) * ( 99 | 1 - self.estimated_fee_ratio 100 | ) # fraction of total asset 101 | logging.info("bid_total_asset_allocation: %s" % self.bid_total_asset_allocation) 102 | self.ask_total_asset_allocation = configs.getfloat( 103 | "ask_total_asset_allocation", 0.11 104 | ) * ( 105 | 1 - self.estimated_fee_ratio 106 | ) # fraction of total asset 107 | logging.info("ask_total_asset_allocation: %s" % self.ask_total_asset_allocation) 108 | -------------------------------------------------------------------------------- /python-demo/requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp 2 | APScheduler 3 | binance 4 | injective_py 5 | numpy 6 | sortedcontainers 7 | -------------------------------------------------------------------------------- /python-demo/strategy/advanced_market_making/.images/Openorders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InjectiveLabs/injective-api-demo/7bb5bfc0f8bcff2ee1cb304e31a545bf067e5b20/python-demo/strategy/advanced_market_making/.images/Openorders.png -------------------------------------------------------------------------------- /python-demo/strategy/advanced_market_making/.images/orderbook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InjectiveLabs/injective-api-demo/7bb5bfc0f8bcff2ee1cb304e31a545bf067e5b20/python-demo/strategy/advanced_market_making/.images/orderbook.png -------------------------------------------------------------------------------- /python-demo/strategy/advanced_market_making/README.md: -------------------------------------------------------------------------------- 1 | # Avellaneda Stoikov HFT Market Making BOT 2 | 3 | This advanced HFT market making bot has taken into account market volalitity risk, inventory risk and order stack management. It is highly configurable for different markets for generating distinct risk profiles when it is deployed in a production environment. 4 | 5 | | Parameter | Description | 6 | | :--- | :---- | 7 | | limit_horizon | finite horizon market making or infinite horizon market making | 8 | | update_interval | how often do we want to update our orders | 9 | | position_max | maximum allowed position | 10 | | sigma | market volatility | 11 | | kappa | order filling rate | 12 | | gamma | our risk averseness towards inventory | 13 | | dt | time step | 14 | | T | Total time of market market, only matters if we choose to market making in finite horizon | 15 | | n_orders | number of orders on each side | 16 | | available_balance | total balance for market making | 17 | | first_order_delta | highest bid price or lowest ask price that is allowed minimum distance from tob | 18 | | last_order_delta | lowest bid price and highest ask price that is allowed maximum distance from tob | 19 | | ask_total_asset_allocation| percentage of available balance allocated to ask side | 20 | | bid_total_asset_allocation| percentage of available balance allocated to bid side | 21 | | first_asset_allocation | extra relative weights of the first order asset allocation to other orders asset allocation | 22 | | last_asset_allocation | relative weights of the last order asset allocation to first orders asset allocation| 23 | | estimated_fee_ratio | estimated trading fee as percentage of order value | 24 | 25 | 26 | ![orderbook](./.images/orderbook.png) 27 | 28 | ![openorders](./.images/Openorders.png) 29 | 30 | High-frequency trading in a limit order book, 31 | 32 | [Marco Avellaneda & Sasha Stoikov paper](https://www.researchgate.net/publication/24086205_High_Frequency_Trading_in_a_Limit_Order_Book). 33 | 34 | 35 | ### Some related discussions about the model: 36 | 37 | [Parameter fitting](https://quant.stackexchange.com/questions/36073/how-does-one-calibrate-lambda-in-a-avellaneda-stoikov-market-making-problem) 38 | 39 | [Model limitations](https://quant.stackexchange.com/questions/36400/avellaneda-stoikov-market-making-model) 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /python-demo/strategy/advanced_market_making/avellaneda_stoikov.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | from math import log 3 | 4 | 5 | def avellaneda_stoikov_model( 6 | sigma: float, 7 | mid_price: float, 8 | limit_horizon: bool, 9 | quantity: float, 10 | quantity_max: float = 10, 11 | kappa: float = 1.5, 12 | gamma: float = 0.1, 13 | dt: float = 3.0, 14 | T: int = 600, # 10 minutes 15 | ) -> Tuple[float, float]: 16 | 17 | # ############### 18 | # # Option A: Limit time horizon 19 | if limit_horizon: 20 | 21 | # Reserve price 22 | reservation_price = mid_price - quantity * gamma * sigma**2 * (T - dt) 23 | 24 | # Reserve spread 25 | reserve_spread = 2 / gamma * log(1 + gamma / kappa) 26 | 27 | # optimal quotes 28 | reservation_price_ask = reservation_price + reserve_spread / 2 29 | reservation_price_bid = reservation_price - reserve_spread / 2 30 | 31 | ############### 32 | # Option B: Unlimit time horizon 33 | else: 34 | 35 | # Upper bound of inventory position 36 | w = 0.5 * gamma**2 * sigma**2 * (quantity_max + 1) ** 2 37 | 38 | # Optimal quotes 39 | coef = ( 40 | gamma**2 * sigma**2 / (2 * w - gamma**2 * sigma**2 * quantity**2) 41 | ) 42 | 43 | reservation_price_ask = mid_price + log(1 + (1 - 2 * quantity) * coef) / gamma 44 | reservation_price_bid = mid_price + log(1 + (-1 - 2 * quantity) * coef) / gamma 45 | 46 | # Reserve price 47 | # reservation_price = (reservation_price_ask + reservation_price_bid) / 2 48 | 49 | return reservation_price_ask, reservation_price_bid 50 | -------------------------------------------------------------------------------- /python-demo/strategy/advanced_market_making/configs.ini: -------------------------------------------------------------------------------- 1 | [AVELLANEDA_STOIKOV] 2 | strategy_name=avellaneda stoikov 3 | private_key=f9db9bf330e23cb7839039e944adef6e9df447b90b503d5b4464c90bea9022f3 4 | is_mainnet = false 5 | nodes=k8s 6 | base=BTC 7 | quote=USDT 8 | update_interval = 3 9 | limit_horizon = true 10 | position_max = 10 11 | sigma = 0.5 12 | kappa = 1.5 13 | gamma = 0.3 14 | dt = 3 15 | T = 600 16 | n_orders = 10 17 | available_balance = 726 18 | first_order_delta = 0.00 19 | last_order_delta = 0.02 20 | ask_total_asset_allocation = 0.60 21 | bid_total_asset_allocation = 0.40 22 | first_asset_allocation = 0.0 23 | last_asset_allocation = 1.2 24 | estimated_fee_ratio = 0.005 25 | #cancel_all_orders_on_exit = false 26 | 27 | 28 | -------------------------------------------------------------------------------- /python-demo/strategy/advanced_market_making/start.py: -------------------------------------------------------------------------------- 1 | import signal 2 | import logging 3 | from asyncio import ( 4 | create_task, 5 | get_event_loop, 6 | ) 7 | 8 | from util.misc import ( 9 | config_check, 10 | shutdown, 11 | handle_exception, 12 | load_ini, 13 | restart_program, 14 | ) 15 | from market_making import PerpMarketMaker 16 | 17 | if __name__ == "__main__": 18 | 19 | logFormatter = logging.Formatter( 20 | "▸ %(asctime)s.%(msecs)03d %(filename)s:%(lineno)d %(levelname)s %(message)s" 21 | ) 22 | rootLogger = logging.getLogger() 23 | 24 | fileHandler = logging.FileHandler("avellaneda_stoikov_model.log") 25 | fileHandler.setFormatter(logFormatter) 26 | rootLogger.addHandler(fileHandler) 27 | 28 | consoleHandler = logging.StreamHandler() 29 | consoleHandler.setFormatter(logFormatter) 30 | rootLogger.addHandler(consoleHandler) 31 | 32 | rootLogger.setLevel(logging.INFO) 33 | 34 | logging.info("start the avellaneda stoikov bot") 35 | 36 | configs = load_ini("./configs.ini") 37 | 38 | # check config 39 | config_check(configs) 40 | 41 | loop = get_event_loop() 42 | 43 | perp_market_maker = PerpMarketMaker( 44 | avellaneda_stoikov_configs=configs["AVELLANEDA_STOIKOV"], 45 | ) 46 | signals = (signal.SIGHUP, signal.SIGTERM, signal.SIGINT) 47 | for s in signals: 48 | loop.add_signal_handler(s, lambda s=s: create_task(shutdown(loop, s))) 49 | 50 | try: 51 | loop.create_task( 52 | perp_market_maker.market_making_strategy(), 53 | name="advanced_market_making_strategy", 54 | ) 55 | loop.run_forever() 56 | finally: 57 | loop.run_until_complete(perp_market_maker.close()) 58 | loop.close() 59 | logging.info("Bye!\n") 60 | logging.warning("Restarting the program") 61 | restart_program() 62 | -------------------------------------------------------------------------------- /python-demo/strategy/cross_exchange_market_making/README.md: -------------------------------------------------------------------------------- 1 | # Cross Exchange Market Making Demo 2 | 3 | ## What does cross exchange market marking demo do? 4 | 5 | This is a simplified InjectiveProtocol-Binance cross-exchange market strategy demo that compares TOB prices on Injective and Binance. 6 | 7 | A change in TOB prices will trigger the following actions: 8 | 1. Cancel existing orders: 9 | 10 | 2. Placing new orders will be one of the following actions: 11 | 1. Places one bid order and one ask order on injective simultaneously, both base asset is greater than minimum base asset balance and quote asset is greater than minimum quote asset balance. 12 | 2. Only places one bid order, if the quote asset balance is greater than the minimum, and the base asset balance is smaller than the minimum base asset balance. 13 | 3. Only places one ask order, if the base asset balance is greater than the minimum, and the quote asset balance is smaller than the minimum quote asset balance. 14 | 4. Do not do anything. 15 | 16 | ## How to run demo 17 | 18 | 1. Modify values in `cross_exchange_market_making` in `python_demo/config/configs.ini`, 19 | 20 | [cross_exchange_market_making] 21 | 22 | strategy_name: your strategy name 23 | 24 | inj_key: input your injection key 25 | 26 | private_key: input your private key 27 | 28 | base_asset: base asset (e.g., "inj" in 'inj/usdt') 29 | 30 | quote_asset: quote asset (e.g., "usdt" in 'inj/usdt') 31 | 32 | min_base_asset_balance: your minimum base asset balance (e.g., 10) 33 | 34 | min_quote_asset_balance: your minimum base asset balance (e.g., 20) 35 | 36 | min_order_size: minimum order size (e.g., 0.1, 1) 37 | 38 | is_mainnet: use mainnet (e.g., false) 39 | 40 | 41 | 2. `python start.py` 42 | 43 | 44 | 45 | ## Suggestions 46 | 47 | Feel free to contact me when you have some errors. 48 | 49 | And there are a few suggestions on how to report demo or API issues. 50 | 51 | 1. before creating any issue, please make sure it is not a duplicate from existing ones 52 | 2. open an issue from injective-exchange `injective_api_demo` directly and label these issues properly with (bugs, enhancement, features, etc), and mentioned `python_demo` in title. 53 | 3. for each issue, please explain what is the issue, how to reproduce it, and present enough proofs (logs, screen shots, raw responses, etc) 54 | 4. let's always go extra one mile when reporting any issues since developer will likely spend more time on fixing those. 55 | -------------------------------------------------------------------------------- /python-demo/strategy/cross_exchange_market_making/configs.ini: -------------------------------------------------------------------------------- 1 | [cross_exchange_market_making] 2 | strategy_name=cross market making 3 | inj_chain_addr=inj14au322k9munkmx5wrchz9q30juf5wjgz2cfqku 4 | private_key= 5 | base_asset=inj 6 | quote_asset=usdt 7 | min_base_asset_balance=10 8 | min_quote_asset_balance=10 9 | min_order_size=1 10 | is_mainnet=false 11 | 12 | 13 | -------------------------------------------------------------------------------- /python-demo/strategy/cross_exchange_market_making/start.py: -------------------------------------------------------------------------------- 1 | import os 2 | from configparser import ConfigParser 3 | import pyinjective 4 | import importlib.resources as pkg_resources 5 | from cross_exchange_market_making_batch import run_cross_exchange_market_making 6 | 7 | _current_dir = os.path.abspath(__file__) 8 | 9 | 10 | if __name__ == "__main__": 11 | config_dir = os.path.join( 12 | os.path.dirname(os.path.dirname(os.path.dirname(_current_dir))), "configs.ini" 13 | ) 14 | 15 | config = ConfigParser() 16 | config.read(config_dir) 17 | # read strategy configs 18 | cross_exchange_market_making_config = config["cross_exchange_market_making"] 19 | 20 | ini_config_dir = denoms_mainnet = pkg_resources.read_text( 21 | pyinjective, "denoms_mainnet.ini" 22 | ) 23 | ini_config = ConfigParser() 24 | # read denoms configs 25 | ini_config.read_string(ini_config_dir) 26 | 27 | run_cross_exchange_market_making(cross_exchange_market_making_config, ini_config) 28 | -------------------------------------------------------------------------------- /python-demo/strategy/cross_exchange_market_making/test.py: -------------------------------------------------------------------------------- 1 | from pyinjective.composer import Composer as ProtoMsgComposer 2 | from pyinjective.client import Client 3 | from pyinjective.transaction import Transaction 4 | from pyinjective.constant import Network 5 | from pyinjective.wallet import PrivateKey, PublicKey, Address 6 | 7 | 8 | async def main() -> None: 9 | # select network: local, testnet, mainnet 10 | network = Network.testnet() 11 | network = Network.testnet() 12 | composer = ProtoMsgComposer(network=network.string()) 13 | 14 | # initialize grpc client 15 | client = Client(network, insecure=True) 16 | account_address = "inj14au322k9munkmx5wrchz9q30juf5wjgz2cfqku" 17 | subacc_list = client.get_subaccount_list(account_address) 18 | print(subacc_list) 19 | 20 | # prepare trade info 21 | market_id = "0xa508cb32923323679f29a032c70342c147c17d0145625922b0ef22e955c844c0" 22 | fee_recipient = "inj1hkhdaj2a2clmq5jq6mspsggqs32vynpk228q3r" 23 | 24 | orders = [ 25 | composer.SpotOrder( 26 | market_id=market_id, 27 | subaccount_id=subaccount_id, 28 | fee_recipient=fee_recipient, 29 | price=7.523, 30 | quantity=0.01, 31 | is_buy=True, 32 | ), 33 | composer.SpotOrder( 34 | market_id=market_id, 35 | subaccount_id=subaccount_id, 36 | fee_recipient=fee_recipient, 37 | price=27.92, 38 | quantity=0.01, 39 | is_buy=False, 40 | ), 41 | ] 42 | 43 | # prepare tx msg 44 | msg = composer.MsgBatchCreateSpotLimitOrders( 45 | sender=address.to_acc_bech32(), orders=orders 46 | ) 47 | 48 | # build sim tx 49 | tx = ( 50 | Transaction() 51 | .with_messages(msg) 52 | .with_sequence(address.get_sequence()) 53 | .with_account_num(address.get_number()) 54 | .with_chain_id(network.chain_id) 55 | ) 56 | sim_sign_doc = tx.get_sign_doc(pub_key) 57 | sim_sig = priv_key.sign(sim_sign_doc.SerializeToString()) 58 | sim_tx_raw_bytes = tx.get_tx_data(sim_sig, pub_key) 59 | 60 | # simulate tx 61 | (simRes, success) = client.simulate_tx(sim_tx_raw_bytes) 62 | if not success: 63 | print(simRes) 64 | return 65 | 66 | sim_res_msg = ProtoMsgComposer.MsgResponses(simRes.result.data, simulation=True) 67 | print("simulation msg response") 68 | print(sim_res_msg) 69 | 70 | # build tx 71 | gas_price = 500000000 72 | gas_limit = simRes.gas_info.gas_used + 15000 # add 15k for gas, fee computation 73 | fee = [ 74 | composer.Coin( 75 | amount=gas_price * gas_limit, 76 | denom=network.fee_denom, 77 | ) 78 | ] 79 | current_height = client.get_latest_block().block.header.height 80 | tx = ( 81 | tx.with_gas(gas_limit) 82 | .with_fee(fee) 83 | .with_memo("") 84 | .with_timeout_height(current_height + 50) 85 | ) 86 | sign_doc = tx.get_sign_doc(pub_key) 87 | sig = priv_key.sign(sign_doc.SerializeToString()) 88 | tx_raw_bytes = tx.get_tx_data(sig, pub_key) 89 | 90 | # broadcast tx: send_tx_async_mode, send_tx_sync_mode, send_tx_block_mode 91 | res = client.send_tx_block_mode(tx_raw_bytes) 92 | res_msg = ProtoMsgComposer.MsgResponses(res.data) 93 | print("tx response") 94 | print(res) 95 | print("tx msg response") 96 | print(res_msg) 97 | -------------------------------------------------------------------------------- /python-demo/strategy/funding_arbitrage/README.md: -------------------------------------------------------------------------------- 1 | # FundingArbitrage Demo 2 | 3 | ## How to run pure arbitrage demo 4 | 5 | Modify environment value in python_demo/config/configs.ini, then 6 | 7 | ```bash 8 | python start.py 9 | ``` 10 | 11 | ## What does pure perpetual market making strategy do 12 | 13 | | Parameter | Required | Description | 14 | | :----------------- | -------- | --------------------------------------------------------- | 15 | | strategy_name | True | | 16 | | priv_key | True | private key of your account | 17 | | is_mainnet | True | 'true' stands for mainnet;only support mainnet | 18 | | binance_api_key | True | | 19 | | binance_api_secret | True | | 20 | | order_size | True | the max position that you wanna hold for funding strategy | 21 | | symbol | True | e.g. BTCUSDT | 22 | | base_asset | True | e.g. BTC | 23 | | quote_asset | True | e.g. USDT | 24 | 25 | In this demo, the program will hold different positions in binance and injective exchange to arbitrage their funding rates. (**Both are USDT-M**) Since the orders in injective exchange is slightly hard to be executed than that in binance, we trade the first leg in injective in maker order type, it will post in the orderbook with closed price to bid price or ask price. Once it get filled, we will execute the second leg which is the inverse position in binance exchange. 26 | 27 | This is a delta neutural strategy, and it will only execute when the sign of funding rates in injective exchange and binance exchange are opposite. You can change the condition in function `on_funding_rates`. 28 | 29 | ```python 30 | async def on_funding_rates(self): 31 | if self.inj_funding_rate * self.binance_funding_rate < 0: 32 | await self.get_address() 33 | self.msg_list = [] 34 | if self.inj_funding_rate > 0 and self.net_position < self.order_size: 35 | # Use the most closed price in inj orderbook to execute the first leg 36 | await self.inj_limit_buy(self.inj_ask_price_1 - self.tick_size, (self.order_size - self.net_position)) 37 | elif self.inj_funding_rate < 0 and self.net_position > -self.order_size: 38 | # Use the most closed price in inj orderbook to execute the first leg 39 | await self.inj_limit_sell(self.inj_bid_price_1 + self.tick_size, (self.order_size - self.net_position)) 40 | ``` 41 | -------------------------------------------------------------------------------- /python-demo/strategy/funding_arbitrage/configs.ini: -------------------------------------------------------------------------------- 1 | [funding arbitrage] 2 | strategy_name=funding arbitrage 3 | priv_key= 4 | binance_api_key= 5 | binance_api_secret= 6 | is_mainnet=false 7 | order_size=0.3 8 | symbol=BTCUSDT 9 | base_asset=BTC 10 | quote_asset=USDT 11 | 12 | 13 | -------------------------------------------------------------------------------- /python-demo/strategy/funding_arbitrage/start.py: -------------------------------------------------------------------------------- 1 | import importlib.resources as pkg_resources 2 | import logging 3 | import os 4 | import sys 5 | import traceback 6 | from configparser import ConfigParser 7 | import pyinjective 8 | 9 | denoms_testnet = pkg_resources.read_text(pyinjective, "denoms_testnet.ini") 10 | 11 | denoms_mainnet = pkg_resources.read_text(pyinjective, "denoms_mainnet.ini") 12 | 13 | _current_dir = os.path.dirname(os.path.abspath(__file__)) 14 | MAIN_DIR = os.path.dirname(os.path.dirname(_current_dir)) 15 | CONFIG_DIR = MAIN_DIR 16 | sys.path.insert(0, MAIN_DIR) 17 | 18 | 19 | if __name__ == "__main__": 20 | from util.misc import restart_program 21 | from funding_arbitrage import Demo 22 | 23 | log_dir = "./log" 24 | log_name = "./funding_arb_demo.log" 25 | config_name = "configs.ini" 26 | if not os.path.exists(log_dir): 27 | os.makedirs(log_dir) 28 | logging.basicConfig( 29 | level=logging.DEBUG, 30 | format="%(asctime)s %(filename)s : %(levelname)s %(message)s", 31 | datefmt="%Y-%m-%d %A %H:%M:%S", 32 | filename=os.path.join(log_dir, log_name), 33 | filemode="a", 34 | ) 35 | console = logging.StreamHandler() 36 | console.setLevel(logging.INFO) 37 | formatter = logging.Formatter( 38 | "%(asctime)s %(filename)s : %(levelname)s %(message)s" 39 | ) 40 | console.setFormatter(formatter) 41 | logging.getLogger().addHandler(console) 42 | logging.getLogger("apscheduler.executors.default").setLevel(logging.WARNING) 43 | 44 | configs = ConfigParser() 45 | configs.read(os.path.join(CONFIG_DIR, config_name)) 46 | mainnet_configs = ConfigParser() 47 | mainnet_configs.read_string(denoms_mainnet) 48 | testnet_configs = ConfigParser() 49 | testnet_configs.read_string(denoms_testnet) 50 | 51 | try: 52 | perp_demo = Demo( 53 | configs["funding arbitrage"], logging, mainnet_configs, testnet_configs 54 | ) 55 | perp_demo.start() 56 | except Exception as e: 57 | logging.CRITICAL(traceback.format_exc()) 58 | logging.info("restarting program...") 59 | restart_program() 60 | -------------------------------------------------------------------------------- /python-demo/strategy/mean_reversion/README.md: -------------------------------------------------------------------------------- 1 | # Mean Reversion Strategy 2 | ## How to run mean reversion strategy 3 | 4 | Modify config values in `./config/configs.ini`, in section `mean_reversion`, then 5 | 6 | ```bash 7 | python start.py 8 | ``` 9 | 10 | ## [What does mean reversion strategy do?](https://www.cmcmarkets.com/en/trading-guides/mean-reversion) 11 | 12 | Mean reversion in trading theorizes that prices tend to return to average levels, and extreme price moves are hard to sustain for extended periods. Traders who partake in mean reversion trading have developed many methods for capitalising on the theory. In all cases, they are betting that an extreme level — whether it be volatility, price, growth, or a technical indicator — will return to the average. 13 | 14 | ## [How to generate trading signals?](https://www.investopedia.com/trading/using-bollinger-bands-to-gauge-trends/) 15 | 1. Define a moving window with length of **n_window**. 16 | 2. Calculate the **mean** and **standard deviation** within the moving window. 17 | 3. Upper band/Lower band = **mean +/- n_std * std** 18 | 4. When the price **go beyond the upper/lower band**, sell/buy the coin to capture the mispricing opportunities. 19 | 5. When the price **go down/up to the mean**, buy/sell the coin. 20 | 6. Move the moving window forward and repeat 1~5. 21 | 22 | **Strategy configs: configs.ini** 23 | 24 | | Parameter | Required| Description| Links| 25 | |:-------:|:-------:|:----------|:-----:| 26 | |strategy_name|True|name of the strategy|| 27 | |private_key|True|input your private key|| 28 | |is_mainnet|True|trading on mainnet or testnet|| 29 | |base_asset|True|In INJ/USDT, INJ is the base_asset|| 30 | |quote_asset|True|In INJ/USDT. USDT is the quote_asset|| 31 | |interval_in_second|True|trading frequency measured in second| 32 | |n_window|True|the number of total period|[SMA](https://www.investopedia.com/terms/s/sma.asp)| 33 | |n_std|True|the number of standard deviations|[Bollinger Bands](https://www.investopedia.com/trading/using-bollinger-bands-to-gauge-trends/)| 34 | |order_size|True|the size of each orders|| 35 | ## Decimal 36 | 37 | One thing you may need to pay more attention to is how to deal with decimals in injective exchange. As we all known, different crypto currecies require diffrent decimal precisions. Separately, ERC-20 tokens (e.g. INJ) have decimals of 18 or another number (like 6 for USDT and USDC). So in injective system that means **having 1 INJ is 1e18 inj** and that **1 USDT is actually 100000 peggy0xdac17f958d2ee523a2206206994597c13d831ec7**. 38 | 39 | For spot markets, a price reflects the **relative exchange rate** between two tokens. If the tokens have the same decimal scale, that's great since the prices become interpretable e.g. USDT/USDC (both have 6 decimals e.g. for USDT https://etherscan.io/address/0xdac17f958d2ee523a2206206994597c13d831ec7#readContract) or MATIC/INJ (both have 18 decimals) since the decimals cancel out. Prices however start to look wonky once you have exchanges between two tokens of different decimals, which unfortunately is most pairs with USDT or USDC denominations. As such, I've created some simple utility functions by keeping a hardcoded dictionary in injective-py and you can aslo achieve such utilities by yourself (e.g. you can use external API like Alchemy's getTokenMetadata to fetch decimal of base and quote asset). 40 | 41 | So for INJ/USDT of 6.9, the price you end up getting is 6.9*10^(6 - 18) = 6.9e-12. Note that this market also happens to have a MinPriceTickSize of 1e-15. This makes sense since since it's defining the minimum price increment of the relative exchange of INJ to USDT. Note that this market also happens to have a MinQuantityTickSize of 1e15. This also makes sense since it refers to the minimum INJ quantity tick size each order must have, which is 1e15/1e18 = 0.001 INJ. 42 | 43 | ## Suggestions 44 | 45 | Feel free to contact me when you have some errors. 46 | 47 | And there are a few suggestions on how to report demo or API issues. 48 | 49 | 1. before creating any issue, please make sure it is not a duplicate from existing ones 50 | 2. open an issue from injective-exchange `injective_api_demo` directly and label these issues properly with (bugs, enhancement, features, etc), and mentioned `python_demo` in title. 51 | 3. for each issue, please explain what is the issue, how to reproduce it, and present enough proofs (logs, screen shots, raw responses, etc) 52 | 4. let's always go extra one mile when reporting any issues since developer will likely spend more time on fixing those. 53 | -------------------------------------------------------------------------------- /python-demo/strategy/mean_reversion/configs.ini: -------------------------------------------------------------------------------- 1 | [mean_reversion] 2 | strategy_name=mean reversion 3 | private_key= 4 | is_mainnet=false 5 | base_asset=inj 6 | quote_asset=usdt 7 | ### Parameters of the mean reversion strategy 8 | # frequency of trading measured in second 9 | interval_in_second=5 10 | # number of total looking-back period 11 | n_window=12 12 | # number of standard deviation in the upper and lower bound 13 | n_std=0.0001 14 | # size of each order 15 | order_size=0.1 16 | 17 | -------------------------------------------------------------------------------- /python-demo/strategy/mean_reversion/start.py: -------------------------------------------------------------------------------- 1 | import os 2 | import traceback 3 | 4 | # from apscheduler.schedulers.asyncio import AsyncIOScheduler 5 | 6 | from mean_reversion_strategy import SmaSpotStrategy 7 | from configparser import ConfigParser 8 | 9 | _current_dir = os.path.dirname(os.path.abspath(__file__)) 10 | 11 | if __name__ == "__main__": 12 | # set directories 13 | main_dir = os.path.dirname(os.path.dirname(_current_dir)) 14 | 15 | # read config files 16 | config_name = "configs.ini" 17 | 18 | configs = ConfigParser() 19 | configs.read(os.path.join(main_dir, config_name)) 20 | # print(configs.sections()) 21 | 22 | try: 23 | mean_reversion_demo = SmaSpotStrategy(configs=configs["mean_reversion"]) 24 | mean_reversion_demo.start() 25 | except Exception as e: 26 | traceback.format_exception_only(type(e), e) 27 | -------------------------------------------------------------------------------- /python-demo/strategy/pure_arbitrage/README.md: -------------------------------------------------------------------------------- 1 | # Pure Arbitrage Demo 2 | 3 | ## How to run pure arbitrage demo 4 | 5 | Modify environment value in python_demo/config/configs.ini, then 6 | 7 | ```bash 8 | python start.py 9 | ``` 10 | 11 | ## What does pure perpetual market making strategy do 12 | 13 | | Parameter | Required | Description | 14 | | :----------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ | 15 | | strategy_name | True | | 16 | | priv_key | True | private key of your account | 17 | | is_mainnet | True | 'true' stands for mainnet;only support mainnet | 18 | | binance_api_key | True | | 19 | | binance_api_secret | True | | 20 | | interval | True | frequency of arbitrage (in second) | 21 | | arb_threshold | True | threshold for price gap between two exchanges
if current price gap is larger than threshold, do arbitrategy on both sides | 22 | | order_size | True | max order size | 23 | | re_balance_hour | True | interval to rebalance positions on both exchange | 24 | | symbol | True | e.g. BTCUSDT | 25 | | base_asset | True | e.g. BTC | 26 | | quote_asset | True | e.g. USDT | 27 | -------------------------------------------------------------------------------- /python-demo/strategy/pure_arbitrage/configs.ini: -------------------------------------------------------------------------------- 1 | [pure arbitrage] 2 | strategy_name=pure arbitrage 3 | priv_key= 4 | binance_api_key= 5 | binance_api_secret= 6 | is_mainnet=false 7 | interval=60 8 | order_size=0.03 9 | symbol=BTCUSDT 10 | base_asset=BTC 11 | quote_asset=USDT 12 | re_balance_hour=8 13 | -------------------------------------------------------------------------------- /python-demo/strategy/pure_arbitrage/start.py: -------------------------------------------------------------------------------- 1 | import importlib.resources as pkg_resources 2 | import logging 3 | import os 4 | import sys 5 | import traceback 6 | from configparser import ConfigParser 7 | import pyinjective 8 | 9 | denoms_testnet = pkg_resources.read_text(pyinjective, "denoms_testnet.ini") 10 | 11 | denoms_mainnet = pkg_resources.read_text(pyinjective, "denoms_mainnet.ini") 12 | 13 | _current_dir = os.path.dirname(os.path.abspath(__file__)) 14 | MAIN_DIR = os.path.dirname(os.path.dirname(_current_dir)) 15 | CONFIG_DIR = MAIN_DIR 16 | sys.path.insert(0, MAIN_DIR) 17 | 18 | 19 | if __name__ == "__main__": 20 | from util.misc import restart_program 21 | from perp_arbitrage import Demo 22 | 23 | log_dir = "./log" 24 | log_name = "./pure_arb_demo.log" 25 | config_name = "configs.ini" 26 | if not os.path.exists(log_dir): 27 | os.makedirs(log_dir) 28 | logging.basicConfig( 29 | level=logging.DEBUG, 30 | format="%(asctime)s %(filename)s : %(levelname)s %(message)s", 31 | datefmt="%Y-%m-%d %A %H:%M:%S", 32 | filename=os.path.join(log_dir, log_name), 33 | filemode="a", 34 | ) 35 | console = logging.StreamHandler() 36 | console.setLevel(logging.INFO) 37 | formatter = logging.Formatter( 38 | "%(asctime)s %(filename)s : %(levelname)s %(message)s" 39 | ) 40 | console.setFormatter(formatter) 41 | logging.getLogger().addHandler(console) 42 | logging.getLogger("apscheduler.executors.default").setLevel(logging.WARNING) 43 | 44 | configs = ConfigParser() 45 | configs.read(os.path.join(CONFIG_DIR, config_name)) 46 | mainnet_configs = ConfigParser() 47 | mainnet_configs.read_string(denoms_mainnet) 48 | testnet_configs = ConfigParser() 49 | testnet_configs.read_string(denoms_testnet) 50 | 51 | try: 52 | perp_demo = Demo( 53 | configs["pure arbitrage"], logging, mainnet_configs, testnet_configs 54 | ) 55 | perp_demo.start() 56 | except Exception as e: 57 | logging.CRITICAL(traceback.format_exc()) 58 | logging.info("restarting program...") 59 | restart_program() 60 | -------------------------------------------------------------------------------- /python-demo/strategy/pure_perp_market_making/README.md: -------------------------------------------------------------------------------- 1 | # Pure Perp Market Making Demo 2 | 3 | ## How to run pure perpetual market making demo 4 | 5 | Modify environment value in python_demo/config/configs.ini, then 6 | 7 | ```bash 8 | python start.py 9 | ``` 10 | 11 | ## What does pure perpetual market making strategy do 12 | 13 | Demo with default json setting is a simple Perpetual BTCUSDT pure market-making strategy, which places one bid order and one ask order around midprice `(midprice = (bid_price_1+ask_price_1) / 2)`, `placing_spread/mid_price` is fixed according to the value in configuration. And it will cancel and quote bid/ask order every interval(default is 20) seconds. **You can add more logic in strategy to manage inventory risk.** 14 | 15 | | Parameter | Required | Description | 16 | | ------------- | -------- | ---------------------------------------------------------- | 17 | | strategy_name | True | | 18 | | priv_key | True | private key of your account | 19 | | is_mainnet | True | 'true' stands for mainnet;
'false' stands for testnet | 20 | | leverage | True | leverage for each order | 21 | | interval | True | frequency of placking orders( in second) | 22 | | order_size | True | | 23 | | spread_ratio | True | spread for bid and ask orders | 24 | | symbol | True | e.g. BTCUSDT | 25 | | base_asset | True | e.g. BTC | 26 | | quote_asset | True | e.g. USDT | 27 | 28 | ## Decimal 29 | 30 | One thing you may need to pay more attention to is how to deal with decimals in injective exchange. As we all known, different crypto currecies require diffrent decimal precisions. Separately, ERC-20 tokens (e.g. INJ) have decimals of 18 or another number (like 6 for USDT and USDC). So in injective system that means **having 1 INJ is 1e18 inj** and that **1 USDT is actually 100000 peggy0xdac17f958d2ee523a2206206994597c13d831ec7**. 31 | 32 | For spot markets, a price reflects the **relative exchange rate** between two tokens. If the tokens have the same decimal scale, that's great since the prices become interpretable e.g. USDT/USDC (both have 6 decimals e.g. for USDT https://etherscan.io/address/0xdac17f958d2ee523a2206206994597c13d831ec7#readContract) or MATIC/INJ (both have 18 decimals) since the decimals cancel out. Prices however start to look wonky once you have exchanges between two tokens of different decimals, which unfortunately is most pairs with USDT or USDC denominations. As such, I've created some simple utility functions by keeping a hardcoded dictionary in injective-py and you can aslo achieve such utilities by yourself (e.g. you can use external API like Alchemy's getTokenMetadata to fetch decimal of base and quote asset). 33 | 34 | So for INJ/USDT of 6.9, the price you end up getting is 6.9*10^(6 - 18) = 6.9e-12. Note that this market also happens to have a MinPriceTickSize of 1e-15. This makes sense since since it's defining the minimum price increment of the relative exchange of INJ to USDT. Note that this market also happens to have a MinQuantityTickSize of 1e15. This also makes sense since it refers to the minimum INJ quantity tick size each order must have, which is 1e15/1e18 = 0.001 INJ. 35 | 36 | ## Suggestions 37 | 38 | Feel free to contact me when you have some errors. 39 | 40 | And there are a few suggestions on how to report demo or API issues. 41 | 42 | 1. before creating any issue, please make sure it is not a duplicate from existing ones 43 | 2. open an issue from injective-exchange `injective_api_demo` directly and label these issues properly with (bugs, enhancement, features, etc), and mentioned `python_demo` in title. 44 | 3. for each issue, please explain what is the issue, how to reproduce it, and present enough proofs (logs, screen shots, raw responses, etc) 45 | 4. let's always go extra one mile when reporting any issues since developer will likely spend more time on fixing those. 46 | -------------------------------------------------------------------------------- /python-demo/strategy/pure_perp_market_making/configs.ini: -------------------------------------------------------------------------------- 1 | [pure perp market making] 2 | strategy_name=pure perp market making 3 | priv_key= 4 | is_mainnet=false 5 | leverage=1 6 | interval=20 7 | re_balance_interval_hour=1 8 | order_size=0.0003 9 | spread_ratio=0.003 10 | symbol=BTCUSDT 11 | base_asset=BTC 12 | quote_asset=USDT 13 | -------------------------------------------------------------------------------- /python-demo/strategy/pure_perp_market_making/perp_simple_strategy.py: -------------------------------------------------------------------------------- 1 | from core.templates.perp_template import PerpTemplate 2 | from datetime import datetime 3 | from apscheduler.schedulers.asyncio import AsyncIOScheduler 4 | import asyncio 5 | from core.object import OrderData, PositionData, TradeData, TickData 6 | from math import fabs 7 | from math import log 8 | import time 9 | from util.decimal_utils import floor_to 10 | from pyinjective.composer import Composer as ProtoMsgComposer 11 | from pyinjective.transaction import Transaction 12 | from pyinjective.client import Client 13 | import traceback 14 | 15 | 16 | class Demo(PerpTemplate): 17 | def __init__(self, setting, logger, mainnet_configs, testnet_configs): 18 | super().__init__(setting, logger, mainnet_configs, testnet_configs) 19 | self.setting = setting 20 | self.is_trading = False 21 | self.init_strategy() 22 | 23 | def init_strategy(self): 24 | self.net_position = 0.0 25 | self.curr_duration_volume = 0.0 26 | self.last_duration_volume = 0.0 27 | self.tick = None 28 | 29 | self.interval = int(self.setting["interval"]) 30 | self.re_balance_interval_hour = self.setting["re_balance_interval_hour"] 31 | self.active_orders = {} # [order_hash, : order_data] 32 | 33 | self.leverage = float(self.setting["leverage"]) 34 | self.order_size = float(self.setting["order_size"]) 35 | self.spread_ratio = float(self.setting["spread_ratio"]) 36 | if self.spread_ratio < 0: 37 | print("Warning! Your spread ratio is smaller than 0!") 38 | print( 39 | "According to Inj proposal 106, self-cross behavior may be banned, and can't get reward from current Trade&Earn Epoch." 40 | ) 41 | print("More details: https://hub.injective.network/proposals/106") 42 | return 43 | 44 | self.gas_price = 500000000 45 | self.strategy_name = self.setting["strategy_name"] 46 | if self.setting.get("start_time", None): 47 | self.start_time = datetime.strptime( 48 | self.setting["start_time"], "%Y-%m-%d %H:%M%S.%f" 49 | ) 50 | else: 51 | self.start_time = datetime.utcnow() 52 | self.setting["start_time"] = self.start_time.strftime("%Y-%m-%d %H:%M%S.%f") 53 | 54 | self.add_schedule() 55 | loop = asyncio.get_event_loop() 56 | loop.run_until_complete(self.get_init_position()) 57 | loop.run_until_complete(self.get_open_orders(self.acc_id, self.market_id)) 58 | loop.run_until_complete(self.get_orderbook()) 59 | self.msg_list = [] 60 | 61 | self.subscribe_stream() 62 | self.logger.debug("finish init") 63 | 64 | def add_schedule(self): 65 | self.sched = AsyncIOScheduler() 66 | self.sched.add_job(self.on_timer, "interval", seconds=self.interval, id="timer") 67 | 68 | self.sched.add_job( 69 | self.close_position, 70 | "interval", 71 | seconds=int(self.re_balance_interval_hour) * 3600, 72 | id="re_balance_position", 73 | ) 74 | 75 | def subscribe_stream(self): 76 | self.tasks = [ 77 | asyncio.Task(self.stream_order(self.market_id, self.acc_id)), 78 | asyncio.Task(self.stream_trade(self.market_id, self.acc_id)), 79 | asyncio.Task(self.stream_position(self.market_id, self.acc_id)), 80 | asyncio.Task(self.stream_orderbook(self.market_id)), 81 | ] 82 | 83 | def start(self): 84 | loop = asyncio.get_event_loop() 85 | self.sched.start() 86 | self.is_trading = True 87 | self.logger.info("start...") 88 | loop.run_until_complete(asyncio.gather(*self.tasks)) 89 | 90 | async def get_init_position(self): 91 | position = await self.get_position() 92 | if len(position.positions) > 0: 93 | position_data = position.positions[0] 94 | self.net_position = ( 95 | float(position_data.quantity) 96 | if position_data.direction == "long" 97 | else -float(position_data.quantity) 98 | ) 99 | self.logger.info(f"net position in {self.symbol}:{self.net_position}") 100 | else: 101 | self.logger.info("net position is zero") 102 | self.net_position = 0.0 103 | 104 | async def on_tick(self, tick_data: TickData): 105 | self.tick = tick_data 106 | 107 | async def on_timer(self): 108 | if not self.tick: 109 | self.logger.critical("self.tick is None") 110 | return 111 | if self.tick.ask_price_1 == 0 or self.tick.bid_price_1 == 0: 112 | self.logger.critical("fail to get latest orderbook price") 113 | return 114 | 115 | await self.get_address() 116 | 117 | if self.is_trading: 118 | await self.market_making() 119 | 120 | def cal_signal(self): 121 | if self.tick: 122 | mid_price = (self.tick.bid_price_1 + self.tick.ask_price_1) / 2 123 | 124 | half_spread = max( 125 | mid_price * self.spread_ratio / 2, 2 * float(self.tick_size) 126 | ) 127 | self.bid_price = mid_price - half_spread 128 | self.ask_price = mid_price + half_spread 129 | 130 | async def market_making(self): 131 | self.msg_list = [] 132 | self.cancel_all() 133 | self.cal_signal() 134 | self.quote_bid_ask() 135 | 136 | for idx, msg in enumerate(self.msg_list): 137 | self.logger.info(f"msg {idx}: {msg}") 138 | 139 | if len(self.msg_list): 140 | await self.send_to_chain() 141 | 142 | async def send_to_chain(self): 143 | tx = ( 144 | Transaction() 145 | .with_messages(*self.msg_list) 146 | .with_sequence(self.address.get_sequence()) 147 | .with_account_num(self.address.get_number()) 148 | .with_chain_id(self.network.chain_id) 149 | ) 150 | sim_sign_doc = tx.get_sign_doc(self.pub_key) 151 | sim_sig = self.priv_key.sign(sim_sign_doc.SerializeToString()) 152 | sim_tx_raw_bytes = tx.get_tx_data(sim_sig, self.pub_key) 153 | 154 | # simulate tx 155 | (sim_res, success) = await self.client.simulate_tx(sim_tx_raw_bytes) 156 | if not success: 157 | self.logger.warning( 158 | "simulation failed, simulation response:{}".format(sim_res) 159 | ) 160 | return 161 | sim_res_msg = ProtoMsgComposer.MsgResponses( 162 | sim_res.result.data, simulation=True 163 | ) 164 | self.logger.info( 165 | "simluation passed, simulation msg response {}".format(sim_res_msg) 166 | ) 167 | 168 | # build tx 169 | gas_limit = ( 170 | sim_res.gas_info.gas_used + 20000 171 | ) # add 15k for gas, fee computation 172 | fee = [ 173 | self.composer.Coin( 174 | amount=self.gas_price * gas_limit, 175 | denom=self.network.fee_denom, 176 | ) 177 | ] 178 | block = await self.client.get_latest_block() 179 | current_height = block.block.header.height 180 | tx = ( 181 | tx.with_gas(gas_limit) 182 | .with_fee(fee) 183 | .with_memo("") 184 | .with_timeout_height(current_height + 50) 185 | ) 186 | sign_doc = tx.get_sign_doc(self.pub_key) 187 | sig = self.priv_key.sign(sign_doc.SerializeToString()) 188 | tx_raw_bytes = tx.get_tx_data(sig, self.pub_key) 189 | 190 | # broadcast tx: send_tx_async_mode, send_tx_sync_mode, send_tx_block_mode 191 | res = await self.client.send_tx_block_mode(tx_raw_bytes) 192 | res_msg = ProtoMsgComposer.MsgResponses(res.data) 193 | self.logger.info("tx response: {}\n tx msg response:{}".format(res, res_msg)) 194 | 195 | async def on_order(self, order_data: OrderData): 196 | if order_data.state == "booked": 197 | self.active_orders[order_data.order_hash] = order_data 198 | 199 | if ( 200 | fabs(order_data.unfilled_quantity) < 1e-7 201 | or order_data.state == "filled" 202 | or order_data.state == "canceled" 203 | ): 204 | try: 205 | self.active_orders.pop(order_data.order_hash) 206 | except Exception as e: 207 | self.logger.error( 208 | "unexcepted order hash, can't pop it from active orders. {}".format( 209 | e 210 | ) 211 | ) 212 | self.logger.error(traceback.format_exc()) 213 | 214 | async def on_account(self, account_data): 215 | pass 216 | 217 | async def on_trade(self, trade_data): 218 | pass 219 | 220 | async def on_position(self, position_data: PositionData): 221 | self.net_position = ( 222 | position_data.quantity 223 | if position_data.direction == "long" 224 | else -position_data.quantity 225 | ) 226 | 227 | def quote_bid_ask(self): 228 | self.msg_list.append( 229 | self.composer.MsgCreateDerivativeLimitOrder( 230 | market_id=self.market_id, 231 | sender=self.sender, 232 | subaccount_id=self.acc_id, 233 | fee_recipient=self.fee_recipient, 234 | price=floor_to(self.bid_price, self.tick_size), 235 | quantity=floor_to(self.order_size, self.step_size), 236 | leverage=self.leverage, 237 | is_buy=True, 238 | ) 239 | ) 240 | 241 | self.logger.info("long {}btc @price{}".format(self.order_size, self.bid_price)) 242 | 243 | self.msg_list.append( 244 | self.composer.MsgCreateDerivativeLimitOrder( 245 | market_id=self.market_id, 246 | sender=self.sender, 247 | subaccount_id=self.acc_id, 248 | fee_recipient=self.fee_recipient, 249 | price=floor_to(self.ask_price, self.tick_size), 250 | quantity=floor_to(self.order_size, self.step_size), 251 | leverage=self.leverage, 252 | is_buy=False, 253 | ) 254 | ) 255 | self.logger.info("short {}btc @price{}".format(self.order_size, self.ask_price)) 256 | 257 | def cancel_all(self): 258 | for order_hash in self.active_orders.keys(): 259 | self.msg_list.append( 260 | self.composer.MsgCancelDerivativeOrder( 261 | sender=self.sender, 262 | market_id=self.market_id, 263 | subaccount_id=self.acc_id, 264 | order_hash=order_hash, 265 | ) 266 | ) 267 | 268 | async def close_position(self): 269 | self.sched.pause_job("timer") 270 | 271 | self.msg_list = [] 272 | 273 | self.cancel_all() 274 | 275 | if self.net_position > 0 and self.tick: 276 | self.msg_list.append( 277 | self.composer.MsgCreateDerivativeLimitOrder( 278 | market_id=self.market_id, 279 | sender=self.sender, 280 | subaccount_id=self.acc_id, 281 | fee_recipient=self.fee_recipient, 282 | price=floor_to( 283 | self.tick.bid_price_1 * (1 - 0.0001), self.tick_size 284 | ), 285 | quantity=floor_to(self.net_position, self.step_size), 286 | is_reduce_only=True, 287 | is_buy=False, 288 | ) 289 | ) 290 | elif self.net_position < 0 and self.tick: 291 | self.msg_list.append( 292 | self.composer.MsgCreateDerivativeLimitOrder( 293 | market_id=self.market_id, 294 | sender=self.sender, 295 | subaccount_id=self.acc_id, 296 | fee_recipient=self.fee_recipient, 297 | price=floor_to( 298 | self.tick.ask_price_1 * (1 + 0.0001), self.tick_size 299 | ), 300 | quantity=floor_to(-self.net_position, self.step_size), 301 | is_reduce_only=True, 302 | is_buy=True, 303 | ) 304 | ) 305 | 306 | if len(self.msg_list): 307 | await self.send_to_chain() 308 | self.sched.resume_job("timer") 309 | 310 | def inventory_management(self): 311 | pass 312 | 313 | def cancel_order(self): 314 | pass 315 | -------------------------------------------------------------------------------- /python-demo/strategy/pure_perp_market_making/start.py: -------------------------------------------------------------------------------- 1 | import importlib.resources as pkg_resources 2 | import logging 3 | import os 4 | import sys 5 | import traceback 6 | from apscheduler.schedulers.asyncio import AsyncIOScheduler 7 | from configparser import ConfigParser 8 | import pyinjective 9 | 10 | denoms_testnet = pkg_resources.read_text(pyinjective, "denoms_testnet.ini") 11 | 12 | denoms_mainnet = pkg_resources.read_text(pyinjective, "denoms_mainnet.ini") 13 | 14 | _current_dir = os.path.dirname(os.path.abspath(__file__)) 15 | MAIN_DIR = os.path.dirname(os.path.dirname(_current_dir)) 16 | CONFIG_DIR = MAIN_DIR 17 | sys.path.insert(0, MAIN_DIR) 18 | 19 | 20 | if __name__ == "__main__": 21 | from util.misc import restart_program 22 | from perp_simple_strategy import Demo 23 | 24 | log_dir = "./log" 25 | log_name = "./perp_demo.log" 26 | config_name = "configs.ini" 27 | if not os.path.exists(log_dir): 28 | os.makedirs(log_dir) 29 | logging.basicConfig( 30 | level=logging.DEBUG, 31 | format="%(asctime)s %(filename)s : %(levelname)s %(message)s", 32 | datefmt="%Y-%m-%d %A %H:%M:%S", 33 | filename=os.path.join(log_dir, log_name), 34 | filemode="a", 35 | ) 36 | console = logging.StreamHandler() 37 | console.setLevel(logging.INFO) 38 | formatter = logging.Formatter( 39 | "%(asctime)s %(filename)s : %(levelname)s %(message)s" 40 | ) 41 | console.setFormatter(formatter) 42 | logging.getLogger().addHandler(console) 43 | logging.getLogger("apscheduler.executors.default").setLevel(logging.WARNING) 44 | 45 | logging.info(f"config dir: {CONFIG_DIR}") 46 | configs = ConfigParser() 47 | configs.read(os.path.join(CONFIG_DIR, config_name)) 48 | mainnet_configs = ConfigParser() 49 | mainnet_configs.read_string(denoms_mainnet) 50 | testnet_configs = ConfigParser() 51 | testnet_configs.read_string(denoms_testnet) 52 | 53 | try: 54 | perp_demo = Demo( 55 | configs["pure perp market making"], 56 | logging, 57 | mainnet_configs, 58 | testnet_configs, 59 | ) 60 | perp_demo.start() 61 | except Exception as e: 62 | logging.CRITICAL(traceback.format_exc()) 63 | logging.info("restarting program...") 64 | restart_program() 65 | -------------------------------------------------------------------------------- /python-demo/strategy/pure_spot_market_making/README.md: -------------------------------------------------------------------------------- 1 | # Pure Perp Market Making Demo 2 | 3 | ## How to run pure perpetual market making demo 4 | 5 | Modify environment value in python_demo/config/configs.ini, then 6 | 7 | ```bash 8 | python start.py 9 | ``` 10 | 11 | ## What does pure perpetual market making strategy do 12 | 13 | Demo with default json setting is a simple Perpetual BTCUSDT pure market-making strategy, which places one bid order and one ask order around midprice `(midprice = (bid_price_1+ask_price_1) / 2)`, `placing_spread/mid_price` is fixed according to the value in configuration. And it will cancel and quote bid/ask order every interval(default is 20) seconds. **You can add more logic in strategy to manage inventory risk.** 14 | 15 | | Parameter | Required | Description | 16 | | ------------- | -------- | ---------------------------------------------------------- | 17 | | strategy_name | True | | 18 | | priv_key | True | private key of your account | 19 | | is_mainnet | True | 'true' stands for mainnet;
'false' stands for testnet | 20 | | leverage | True | leverage for each order | 21 | | interval | True | frequency of placking orders( in second) | 22 | | order_size | True | | 23 | | spread_ratio | True | spread for bid and ask orders | 24 | | symbol | True | e.g. BTCUSDT | 25 | | base_asset | True | e.g. BTC | 26 | | quote_asset | True | e.g. USDT | 27 | 28 | ## Decimal 29 | 30 | One thing you may need to pay more attention to is how to deal with decimals in injective exchange. As we all known, different crypto currecies require diffrent decimal precisions. Separately, ERC-20 tokens (e.g. INJ) have decimals of 18 or another number (like 6 for USDT and USDC). So in injective system that means **having 1 INJ is 1e18 inj** and that **1 USDT is actually 100000 peggy0xdac17f958d2ee523a2206206994597c13d831ec7**. 31 | 32 | For spot markets, a price reflects the **relative exchange rate** between two tokens. If the tokens have the same decimal scale, that's great since the prices become interpretable e.g. USDT/USDC (both have 6 decimals e.g. for USDT https://etherscan.io/address/0xdac17f958d2ee523a2206206994597c13d831ec7#readContract) or MATIC/INJ (both have 18 decimals) since the decimals cancel out. Prices however start to look wonky once you have exchanges between two tokens of different decimals, which unfortunately is most pairs with USDT or USDC denominations. As such, I've created some simple utility functions by keeping a hardcoded dictionary in injective-py and you can aslo achieve such utilities by yourself (e.g. you can use external API like Alchemy's getTokenMetadata to fetch decimal of base and quote asset). 33 | 34 | So for INJ/USDT of 6.9, the price you end up getting is 6.9*10^(6 - 18) = 6.9e-12. Note that this market also happens to have a MinPriceTickSize of 1e-15. This makes sense since since it's defining the minimum price increment of the relative exchange of INJ to USDT. Note that this market also happens to have a MinQuantityTickSize of 1e15. This also makes sense since it refers to the minimum INJ quantity tick size each order must have, which is 1e15/1e18 = 0.001 INJ. 35 | 36 | ## Suggestions 37 | 38 | Feel free to contact me when you have some errors. 39 | 40 | And there are a few suggestions on how to report demo or API issues. 41 | 42 | 1. before creating any issue, please make sure it is not a duplicate from existing ones 43 | 2. open an issue from injective-exchange `injective_api_demo` directly and label these issues properly with (bugs, enhancement, features, etc), and mentioned `python_demo` in title. 44 | 3. for each issue, please explain what is the issue, how to reproduce it, and present enough proofs (logs, screen shots, raw responses, etc) 45 | 4. let's always go extra one mile when reporting any issues since developer will likely spend more time on fixing those. 46 | -------------------------------------------------------------------------------- /python-demo/strategy/pure_spot_market_making/configs.ini: -------------------------------------------------------------------------------- 1 | [pure spot market making] 2 | strategy_name=pure spot market making 3 | priv_key= 4 | is_mainnet=false 5 | leverage=1 6 | interval=20 7 | re_balance_interval_hour=1 8 | order_size=0.0003 9 | spread_ratio=0.003 10 | symbol=BTCUSDT 11 | base_asset=BTC 12 | quote_asset=USDT 13 | 14 | 15 | -------------------------------------------------------------------------------- /python-demo/strategy/pure_spot_market_making/spot_simple_strategy.py: -------------------------------------------------------------------------------- 1 | from core.templates.spot_template import SpotTemplate 2 | from datetime import datetime 3 | from apscheduler.schedulers.asyncio import AsyncIOScheduler 4 | import asyncio 5 | from core.object import OrderData, PositionData, TradeData, TickData 6 | from math import fabs 7 | from math import log 8 | import time 9 | from util.decimal_utils import floor_to 10 | from pyinjective.composer import Composer as ProtoMsgComposer 11 | from pyinjective.transaction import Transaction 12 | from pyinjective.client import Client 13 | import traceback 14 | 15 | 16 | class Demo(SpotTemplate): 17 | def __init__(self, setting, logger, mainnet_configs, testnet_configs): 18 | super().__init__(setting, logger, mainnet_configs, testnet_configs) 19 | self.setting = setting 20 | self.is_trading = False 21 | self.init_strategy() 22 | 23 | def init_strategy(self): 24 | self.net_position = 0.0 25 | self.curr_duration_volume = 0.0 26 | self.last_duration_volume = 0.0 27 | self.tick = None 28 | 29 | self.interval = int(self.setting["interval"]) 30 | self.re_balance_interval_hour = self.setting["re_balance_interval_hour"] 31 | self.active_orders = {} # [order_hash, : order_data] 32 | 33 | self.leverage = float(self.setting["leverage"]) 34 | self.order_size = float(self.setting["order_size"]) 35 | self.spread_ratio = float(self.setting["spread_ratio"]) 36 | if self.spread_ratio < 0: 37 | print("Warning! Your spread ratio is smaller than 0!") 38 | print( 39 | "According to Inj proposal 106, self-cross behavior may be banned, and can't get reward from current Trade&Earn Epoch." 40 | ) 41 | print("More details: https://hub.injective.network/proposals/106") 42 | return 43 | 44 | self.gas_price = 500000000 45 | self.strategy_name = self.setting["strategy_name"] 46 | if self.setting.get("start_time", None): 47 | self.start_time = datetime.strptime( 48 | self.setting["start_time"], "%Y-%m-%d %H:%M%S.%f" 49 | ) 50 | else: 51 | self.start_time = datetime.utcnow() 52 | self.setting["start_time"] = self.start_time.strftime("%Y-%m-%d %H:%M%S.%f") 53 | 54 | self.add_schedule() 55 | loop = asyncio.get_event_loop() 56 | # loop.run_until_complete(self.get_init_position()) 57 | loop.run_until_complete(self.get_open_orders(self.acc_id, self.market_id)) 58 | loop.run_until_complete(self.get_orderbook()) 59 | self.msg_list = [] 60 | 61 | self.subscribe_stream() 62 | self.logger.debug("finish init") 63 | 64 | def add_schedule(self): 65 | self.sched = AsyncIOScheduler() 66 | self.sched.add_job(self.on_timer, "interval", seconds=self.interval, id="timer") 67 | 68 | self.sched.add_job( 69 | self.close_position, 70 | "interval", 71 | seconds=int(self.re_balance_interval_hour) * 3600, 72 | id="re_balance_position", 73 | ) 74 | 75 | def subscribe_stream(self): 76 | self.tasks = [ 77 | asyncio.Task(self.stream_order(self.market_id, self.acc_id)), 78 | asyncio.Task(self.stream_trade(self.market_id, self.acc_id)), 79 | # asyncio.Task(self.stream_position(self.market_id, self.acc_id)), 80 | asyncio.Task(self.stream_orderbook(self.market_id)), 81 | ] 82 | 83 | def start(self): 84 | loop = asyncio.get_event_loop() 85 | self.sched.start() 86 | self.is_trading = True 87 | self.logger.info("start...") 88 | loop.run_until_complete(asyncio.gather(*self.tasks)) 89 | 90 | # async def get_init_position(self): 91 | # position = await self.get_position() 92 | # if len(position.positions) > 0: 93 | # position_data = position.positions[0] 94 | # self.net_position = ( 95 | # float(position_data.quantity) 96 | # if position_data.direction == "long" 97 | # else -float(position_data.quantity) 98 | # ) 99 | # self.logger.info(f"net position in {self.symbol}:{self.net_position}") 100 | # else: 101 | # self.logger.info("net position is zero") 102 | # self.net_position = 0.0 103 | 104 | async def on_tick(self, tick_data: TickData): 105 | self.tick = tick_data 106 | 107 | async def on_timer(self): 108 | if not self.tick: 109 | self.logger.critical("self.tick is None") 110 | return 111 | if self.tick.ask_price_1 == 0 or self.tick.bid_price_1 == 0: 112 | self.logger.critical("fail to get latest orderbook price") 113 | return 114 | 115 | await self.get_address() 116 | 117 | if self.is_trading: 118 | await self.market_making() 119 | 120 | def cal_signal(self): 121 | if self.tick: 122 | mid_price = (self.tick.bid_price_1 + self.tick.ask_price_1) / 2 123 | 124 | half_spread = max( 125 | mid_price * self.spread_ratio / 2, 2 * float(self.tick_size) 126 | ) 127 | self.bid_price = mid_price - half_spread 128 | self.ask_price = mid_price + half_spread 129 | 130 | async def market_making(self): 131 | self.msg_list = [] 132 | self.cancel_all() 133 | self.cal_signal() 134 | self.quote_bid_ask() 135 | 136 | for idx, msg in enumerate(self.msg_list): 137 | self.logger.info(f"msg {idx}: {msg}") 138 | 139 | if len(self.msg_list): 140 | await self.send_to_chain() 141 | 142 | async def send_to_chain(self): 143 | tx = ( 144 | Transaction() 145 | .with_messages(*self.msg_list) 146 | .with_sequence(self.address.get_sequence()) 147 | .with_account_num(self.address.get_number()) 148 | .with_chain_id(self.network.chain_id) 149 | ) 150 | sim_sign_doc = tx.get_sign_doc(self.pub_key) 151 | sim_sig = self.priv_key.sign(sim_sign_doc.SerializeToString()) 152 | sim_tx_raw_bytes = tx.get_tx_data(sim_sig, self.pub_key) 153 | 154 | # simulate tx 155 | (sim_res, success) = await self.client.simulate_tx(sim_tx_raw_bytes) 156 | if not success: 157 | self.logger.warning( 158 | "simulation failed, simulation response:{}".format(sim_res) 159 | ) 160 | return 161 | sim_res_msg = ProtoMsgComposer.MsgResponses( 162 | sim_res.result.data, simulation=True 163 | ) 164 | self.logger.info( 165 | "simluation passed, simulation msg response {}".format(sim_res_msg) 166 | ) 167 | 168 | # build tx 169 | gas_limit = ( 170 | sim_res.gas_info.gas_used + 20000 171 | ) # add 15k for gas, fee computation 172 | fee = [ 173 | self.composer.Coin( 174 | amount=self.gas_price * gas_limit, 175 | denom=self.network.fee_denom, 176 | ) 177 | ] 178 | block = await self.client.get_latest_block() 179 | current_height = block.block.header.height 180 | tx = ( 181 | tx.with_gas(gas_limit) 182 | .with_fee(fee) 183 | .with_memo("") 184 | .with_timeout_height(current_height + 50) 185 | ) 186 | sign_doc = tx.get_sign_doc(self.pub_key) 187 | sig = self.priv_key.sign(sign_doc.SerializeToString()) 188 | tx_raw_bytes = tx.get_tx_data(sig, self.pub_key) 189 | 190 | # broadcast tx: send_tx_async_mode, send_tx_sync_mode, send_tx_block_mode 191 | res = await self.client.send_tx_block_mode(tx_raw_bytes) 192 | res_msg = ProtoMsgComposer.MsgResponses(res.data) 193 | self.logger.info("tx response: {}\n tx msg response:{}".format(res, res_msg)) 194 | 195 | async def on_order(self, order_data: OrderData): 196 | if order_data.state == "booked": 197 | self.active_orders[order_data.order_hash] = order_data 198 | 199 | if ( 200 | fabs(order_data.unfilled_quantity) < 1e-7 201 | or order_data.state == "filled" 202 | or order_data.state == "canceled" 203 | ): 204 | try: 205 | self.active_orders.pop(order_data.order_hash) 206 | except Exception as e: 207 | self.logger.error( 208 | "unexcepted order hash, can't pop it from active orders. {}".format( 209 | e 210 | ) 211 | ) 212 | self.logger.error(traceback.format_exc()) 213 | 214 | async def on_account(self, account_data): 215 | pass 216 | 217 | async def on_trade(self, trade_data): 218 | pass 219 | 220 | # async def on_position(self, position_data: PositionData): 221 | # self.net_position = ( 222 | # position_data.quantity 223 | # if position_data.direction == "long" 224 | # else -position_data.quantity 225 | # ) 226 | 227 | def quote_bid_ask(self): 228 | self.msg_list.append( 229 | self.composer.MsgCreateSpotLimitOrder( 230 | # self.composer.MsgCreateDerivativeLimitOrder( 231 | market_id=self.market_id, 232 | sender=self.sender, 233 | subaccount_id=self.acc_id, 234 | fee_recipient=self.fee_recipient, 235 | price=floor_to(self.bid_price, self.tick_size), 236 | quantity=floor_to(self.order_size, self.step_size), 237 | is_buy=True, 238 | ) 239 | ) 240 | 241 | self.logger.info("long {}btc @price{}".format(self.order_size, self.bid_price)) 242 | 243 | self.msg_list.append( 244 | self.composer.MsgCreateSpotLimitOrder( 245 | market_id=self.market_id, 246 | sender=self.sender, 247 | subaccount_id=self.acc_id, 248 | fee_recipient=self.fee_recipient, 249 | price=floor_to(self.ask_price, self.tick_size), 250 | quantity=floor_to(self.order_size, self.step_size), 251 | is_buy=False, 252 | ) 253 | ) 254 | self.logger.info("short {}btc @price{}".format(self.order_size, self.ask_price)) 255 | 256 | def cancel_all(self): 257 | for order_hash in self.active_orders.keys(): 258 | self.msg_list.append( 259 | self.composer.MsgCancelSpotOrder( 260 | sender=self.sender, 261 | market_id=self.market_id, 262 | subaccount_id=self.acc_id, 263 | order_hash=order_hash, 264 | ) 265 | ) 266 | 267 | async def close_position(self): 268 | self.sched.pause_job("timer") 269 | 270 | self.msg_list = [] 271 | 272 | self.cancel_all() 273 | 274 | if self.net_position > 0 and self.tick: 275 | self.msg_list.append( 276 | self.composer.MsgCreateDerivativeLimitOrder( 277 | market_id=self.market_id, 278 | sender=self.sender, 279 | subaccount_id=self.acc_id, 280 | fee_recipient=self.fee_recipient, 281 | price=floor_to( 282 | self.tick.bid_price_1 * (1 - 0.0001), self.tick_size 283 | ), 284 | quantity=floor_to(self.net_position, self.step_size), 285 | is_reduce_only=True, 286 | is_buy=False, 287 | ) 288 | ) 289 | elif self.net_position < 0 and self.tick: 290 | self.msg_list.append( 291 | self.composer.MsgCreateDerivativeLimitOrder( 292 | market_id=self.market_id, 293 | sender=self.sender, 294 | subaccount_id=self.acc_id, 295 | fee_recipient=self.fee_recipient, 296 | price=floor_to( 297 | self.tick.ask_price_1 * (1 + 0.0001), self.tick_size 298 | ), 299 | quantity=floor_to(-self.net_position, self.step_size), 300 | is_reduce_only=True, 301 | is_buy=True, 302 | ) 303 | ) 304 | 305 | if len(self.msg_list): 306 | await self.send_to_chain() 307 | self.sched.resume_job("timer") 308 | 309 | def inventory_management(self): 310 | pass 311 | 312 | def cancel_order(self): 313 | pass 314 | -------------------------------------------------------------------------------- /python-demo/strategy/pure_spot_market_making/start.py: -------------------------------------------------------------------------------- 1 | import importlib.resources as pkg_resources 2 | import logging 3 | import os 4 | import sys 5 | import traceback 6 | from apscheduler.schedulers.asyncio import AsyncIOScheduler 7 | from configparser import ConfigParser 8 | import pyinjective 9 | 10 | denoms_testnet = pkg_resources.read_text(pyinjective, "denoms_testnet.ini") 11 | 12 | denoms_mainnet = pkg_resources.read_text(pyinjective, "denoms_mainnet.ini") 13 | 14 | _current_dir = os.path.dirname(os.path.abspath(__file__)) 15 | MAIN_DIR = os.path.dirname(os.path.dirname(_current_dir)) 16 | CONFIG_DIR = MAIN_DIR 17 | sys.path.insert(0, MAIN_DIR) 18 | 19 | 20 | if __name__ == "__main__": 21 | from util.misc import restart_program 22 | from spot_simple_strategy import Demo 23 | 24 | log_dir = "./log" 25 | log_name = "./perp_demo.log" 26 | config_name = "configs.ini" 27 | if not os.path.exists(log_dir): 28 | os.makedirs(log_dir) 29 | logging.basicConfig( 30 | level=logging.DEBUG, 31 | format="%(asctime)s %(filename)s : %(levelname)s %(message)s", 32 | datefmt="%Y-%m-%d %A %H:%M:%S", 33 | filename=os.path.join(log_dir, log_name), 34 | filemode="a", 35 | ) 36 | console = logging.StreamHandler() 37 | console.setLevel(logging.INFO) 38 | formatter = logging.Formatter( 39 | "%(asctime)s %(filename)s : %(levelname)s %(message)s" 40 | ) 41 | console.setFormatter(formatter) 42 | logging.getLogger().addHandler(console) 43 | logging.getLogger("apscheduler.executors.default").setLevel(logging.WARNING) 44 | 45 | logging.info(f"config dir: {CONFIG_DIR}") 46 | configs = ConfigParser() 47 | configs.read(os.path.join(CONFIG_DIR, config_name)) 48 | mainnet_configs = ConfigParser() 49 | mainnet_configs.read_string(denoms_mainnet) 50 | testnet_configs = ConfigParser() 51 | testnet_configs.read_string(denoms_testnet) 52 | 53 | try: 54 | spot_demo = Demo( 55 | configs["pure perp market making"], 56 | logging, 57 | mainnet_configs, 58 | testnet_configs, 59 | ) 60 | spot_demo.start() 61 | except Exception as e: 62 | logging.CRITICAL(traceback.format_exc()) 63 | logging.info("restarting program...") 64 | restart_program() 65 | -------------------------------------------------------------------------------- /python-demo/util/data_manager.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from collections import deque 3 | 4 | import numpy as np 5 | 6 | 7 | class DataManager(ABC): 8 | """ 9 | Data Manager 10 | 11 | used to manipulate data and generate signals 12 | """ 13 | 14 | def __init__(self, n_win, n_std, **kwargs): 15 | """ 16 | 17 | Args: 18 | n_win: Decimal | the number of period in the moving window 19 | n_std: Decimal | the number of standard deviation in the upper anad lower bound 20 | **kwargs: 21 | """ 22 | self.n_win = n_win 23 | self.n_std = n_std 24 | 25 | self.container = dict() 26 | self.container["BestBid"] = deque([0], maxlen=self.n_win) 27 | self.container["BestAsk"] = deque([0], maxlen=self.n_win) 28 | self.container["BestBidQuantity"] = deque([0], maxlen=self.n_win) 29 | self.container["BestAskQuantity"] = deque([0], maxlen=self.n_win) 30 | self.container["MidPrice"] = deque([0], maxlen=self.n_win) 31 | self.container["AVG"] = deque([0], maxlen=self.n_win) 32 | self.container["STD"] = deque([0], maxlen=self.n_win) 33 | self.container["Upper"] = deque([0], maxlen=self.n_win) 34 | self.container["Lower"] = deque([0], maxlen=self.n_win) 35 | 36 | @abstractmethod 37 | def update(self): 38 | pass 39 | 40 | @abstractmethod 41 | def generate_signal(self): 42 | pass 43 | 44 | 45 | class SmaDataManager(DataManager): 46 | def __init__(self, n_window=20, n_std=2.0, **kwargs): 47 | super().__init__(n_win=n_window, n_std=n_std, **kwargs) 48 | 49 | def _add(self, best_bid, best_ask, best_bid_quantity, best_ask_quantity): 50 | """ 51 | Accumulate data point to calculate moving average and moving standard deviation in the starting phase 52 | 53 | Args: 54 | best_bid: 55 | best_ask: 56 | best_bid_quantity: 57 | best_ask_quantity: 58 | 59 | Returns: 60 | 61 | """ 62 | # calculate mid price 63 | mid_price = (best_bid + best_ask) / 2 64 | # add top of orderbook and quantity to the data container 65 | self.container["BestBid"].append(best_bid) 66 | self.container["BestAsk"].append(best_ask) 67 | self.container["BestBidQuantity"].append(best_bid_quantity) 68 | self.container["BestAskQuantity"].append(best_ask_quantity) 69 | self.container["MidPrice"].append(mid_price) 70 | 71 | # calculate the moving average and moving standard deviation 72 | self.container["AVG"].append(self.container["AVG"][-1] + mid_price / self.n_win) 73 | self.container["STD"].append(0) 74 | 75 | # calculate the upper band and the lower band 76 | self.container["Upper"].append(0) 77 | self.container["Lower"].append(0) 78 | 79 | def _replace(self, best_bid, best_ask, best_bid_quantity, best_ask_quantity): 80 | """ 81 | Moving the sliding window and get the latest moving mean and moving standard deviation 82 | Args: 83 | best_bid: 84 | best_ask: 85 | best_bid_quantity: 86 | best_ask_quantity: 87 | 88 | Returns: 89 | 90 | """ 91 | # calculate mid price 92 | mid_price = (best_bid + best_ask) / 2 93 | 94 | # add top of orderbook and quantity to the data container 95 | self.container["BestBid"].append(best_bid) 96 | self.container["BestAsk"].append(best_ask) 97 | self.container["BestBidQuantity"].append(best_bid_quantity) 98 | self.container["BestAskQuantity"].append(best_ask_quantity) 99 | 100 | # calculate the moving average and moving standard deviation 101 | self.container["AVG"].append( 102 | self.container["AVG"][-1] 103 | - (self.container["MidPrice"][0] / self.n_win) 104 | + (mid_price / self.n_win) 105 | ) 106 | self.container["MidPrice"].append(mid_price) 107 | std = np.std(self.container["MidPrice"]) 108 | 109 | # calculate the upper band and the lower band 110 | self.container["STD"].append(std) 111 | self.container["Upper"].append(mid_price + self.n_std * std) 112 | self.container["Lower"].append(mid_price - self.n_std * std) 113 | 114 | def update(self, best_bid, best_ask, best_bid_quantity, best_ask_quantity): 115 | """ 116 | Feed the latest top of orderbook data to data container 117 | 118 | Args: 119 | best_bid: 120 | best_ask: 121 | best_bid_quantity: 122 | best_ask_quantity: 123 | 124 | Returns: 125 | 126 | """ 127 | if self.container["MidPrice"][0]: 128 | self._replace(best_bid, best_ask, best_bid_quantity, best_ask_quantity) 129 | return True 130 | else: 131 | self._add(best_bid, best_ask, best_bid_quantity, best_ask_quantity) 132 | return False 133 | 134 | def generate_signal(self): 135 | """ 136 | Building rules to calculate real-time trading signal 137 | 138 | Returns: 139 | -1 ==> sell 140 | 1 ==> buy 141 | 0 ==> hold 142 | 143 | """ 144 | if ( 145 | self.container["AVG"][-2] 146 | < self.container["MidPrice"][-2] 147 | < self.container["Upper"][-2] 148 | ): 149 | if self.container["MidPrice"][-1] > self.container["Upper"][-1]: 150 | return -1 151 | elif self.container["MidPrice"][-1] < self.container["AVG"][-1]: 152 | return 1 153 | else: 154 | return 0 155 | if ( 156 | self.container["AVG"][-2] 157 | > self.container["MidPrice"][-2] 158 | > self.container["Lower"][-2] 159 | ): 160 | if self.container["MidPrice"][-1] < self.container["Lower"][-1]: 161 | return 1 162 | elif self.container["MidPrice"][-1] > self.container["AVG"][-1]: 163 | return -1 164 | else: 165 | return 0 166 | else: 167 | return 0 168 | 169 | 170 | class EmaDataManager(DataManager): 171 | def __init__(self, n_window=20, maxlen=100, **kwargs): 172 | super().__init__(n_win=n_window, maxlen=maxlen, **kwargs) 173 | self.smoothing = kwargs.get("smoothing", 3) 174 | self.smoothing_adj = self.smoothing / (1 + self.n_win) 175 | self.smoothing_adj_1 = 1 - self.smoothing_adj 176 | 177 | 178 | class EwmaDataManager(DataManager): 179 | def __init__(self, n_window=20, maxlen=100, **kwargs): 180 | super().__init__(n_win=20, maxlen=100, **kwargs) 181 | self.alpha = kwargs.get("alpha", 0.4) 182 | self.alpha_1 = 1 - self.alpha 183 | -------------------------------------------------------------------------------- /python-demo/util/decimal_utils.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | from math import floor, ceil 3 | 4 | 5 | def round_to(value: float, target: float) -> float: 6 | """ 7 | Round price to price tick value. 8 | """ 9 | value_tmp = Decimal(str(value)) 10 | target_tmp = Decimal(str(target)) 11 | rounded = float(int(round(value_tmp / target_tmp)) * target_tmp) 12 | return rounded 13 | 14 | 15 | def floor_to(value: float, target: float) -> float: 16 | """ 17 | Similar to math.floor function, but to target float number. 18 | """ 19 | value_tmp = Decimal(str(value)) 20 | target_tmp = Decimal(str(target)) 21 | result = float(int(floor(value_tmp / target_tmp)) * target_tmp) 22 | return result 23 | 24 | 25 | def ceil_to(value: float, target: float) -> float: 26 | """ 27 | Similar to math.ceil function, but to target float number. 28 | """ 29 | value_tmp = Decimal(str(value)) 30 | target_tmp = Decimal(str(target)) 31 | result = float(int(ceil(value_tmp / target_tmp)) * target_tmp) 32 | return result 33 | 34 | 35 | def get_digits(value: float) -> int: 36 | """ 37 | Get number of digits after decimal point. 38 | """ 39 | value_str = str(value) 40 | 41 | if "e-" in value_str: 42 | _, buf = value_str.split("e-") 43 | return int(buf) 44 | elif "." in value_str: 45 | _, buf = value_str.split(".") 46 | return len(buf) 47 | else: 48 | return 0 49 | -------------------------------------------------------------------------------- /python-demo/util/misc.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import logging 4 | 5 | 6 | def restart_program(): 7 | python = sys.executable 8 | os.execl(python, python, *sys.argv) 9 | 10 | import logging 11 | 12 | 13 | from typing import Tuple, List 14 | from pyinjective.core.network import Network 15 | from pyinjective.composer import Composer 16 | from pyinjective.async_client import AsyncClient 17 | from asyncio import ( 18 | create_task, 19 | all_tasks, 20 | current_task, 21 | gather, 22 | sleep, 23 | Task, 24 | CancelledError, 25 | ) 26 | import aiohttp 27 | from configparser import ConfigParser 28 | import signal 29 | import sys 30 | import os 31 | 32 | 33 | async def shutdown(loop, signal=None): 34 | if signal: 35 | logging.info(f"Received exit signal {signal.name}...") 36 | tasks = [task for task in all_tasks() if task is not current_task()] 37 | 38 | for task in tasks: 39 | task.cancel() 40 | 41 | logging.info(f"Cancelling {len(tasks)} outstanding tasks") 42 | res = await gather(*tasks, return_exceptions=True) 43 | logging.info(f"Cancelled {len(tasks)} outstanding tasks {res}") 44 | loop.stop() 45 | 46 | 47 | def handle_exception(loop, context): 48 | logging.error(f"Exception full context: {context}") 49 | msg = context.get("exception", context["message"]) 50 | logging.error(f"Caught exception: {msg}") 51 | logging.info("Shutting down...") 52 | # This won't cancel all orders 53 | exception_caused_shutdown = create_task( 54 | shutdown(loop, signal=signal.SIGINT), name="shutdown from handle_exception" 55 | ) 56 | 57 | 58 | def load_ini(ini_filename: str): 59 | """ 60 | Load data from ini file, including path 61 | """ 62 | try: 63 | config = ConfigParser() 64 | config.read(ini_filename) 65 | return config 66 | except IOError: 67 | raise IOError(f"Failed to open/find {ini_filename}") 68 | 69 | 70 | # Checking this specia error and restart the program 71 | async def server_check(): 72 | """ 73 | Check if the server is running 74 | """ 75 | # TODO check the server i'm using, and all the backup servers 76 | 77 | for sentry in ["sentry0", "sentry1", "sentry3"]: 78 | url = f"{sentry}.injective.network:26657/status" 79 | async with aiohttp.ClientSession() as session: 80 | async with session.get(url) as resp: 81 | if resp.status == 200: 82 | res = await resp.json() 83 | if res["result"]["sync_info"]["catching_up"]: 84 | for sentry in ["sentry0", "sentry1", "sentry3"]: 85 | url = f"{sentry}.injective.network:26657/status" 86 | async with session.get(url) as resp: 87 | if resp.status == 200: 88 | res = await resp.json() 89 | if not res["result"]["sync_info"]["catching_up"]: 90 | _node = sentry 91 | raise Exception(f"Change node to {_node}") 92 | else: 93 | pass 94 | 95 | 96 | def _handle_task_result(task: Task) -> None: 97 | try: 98 | task.result() 99 | except CancelledError: 100 | pass # Task cancellation should not be logged as an error. 101 | except Exception: # pylint: disable=broad-except 102 | logging.exception("Exception raised by task = %r", task) 103 | 104 | raise Exception(f"Task {task.get_name()} failed, {task.exception()}") 105 | 106 | 107 | # def restart_program(): 108 | # python = sys.executable 109 | # os.execl(python, python, *sys.argv) 110 | 111 | 112 | def round_down(number: float, decimals: int, min_ticker) -> float: 113 | v = int(number * (10**decimals)) / 10**decimals 114 | if v < min_ticker: 115 | return min_ticker 116 | return v 117 | 118 | 119 | def truncate_float(number: float, decimals: int) -> float: 120 | return int(number * (10**decimals)) / 10**decimals 121 | 122 | 123 | def switch_network( 124 | nodes: List[str], index: int, is_mainnet: bool = False 125 | ) -> Tuple[int, Network, bool]: 126 | n = index % len(nodes) 127 | 128 | if is_mainnet: 129 | if nodes[n] == "k8s": 130 | is_insecure = False 131 | else: 132 | is_insecure = True 133 | network = Network.mainnet(nodes[n]) 134 | else: 135 | is_insecure = False 136 | network = Network.testnet() 137 | return n, network, is_insecure 138 | 139 | 140 | def update_composer(network: str) -> Composer: 141 | return Composer(network=network) 142 | 143 | 144 | def build_client(network: Network, is_insecure: bool) -> AsyncClient: 145 | return AsyncClient(network=network, insecure=is_insecure) 146 | 147 | 148 | def build_client_and_composer( 149 | network: Network, is_insecure: bool 150 | ) -> Tuple[AsyncClient, Composer]: 151 | return build_client(network, is_insecure), update_composer(network.string()) 152 | 153 | 154 | def config_check(config): 155 | """ 156 | check if config file is valid 157 | """ 158 | 159 | def range_check(section_name, value, lower_bound, upper_bound): 160 | if (value >= lower_bound) and (value <= upper_bound): 161 | logging.info(f"{section_name}: {value}") 162 | elif value < lower_bound: 163 | raise Exception( 164 | f"{section_name} must be greater than {lower_bound}, got {value}" 165 | ) 166 | else: 167 | raise Exception( 168 | f"{section_name} must be less than {upper_bound}, got {value}" 169 | ) 170 | 171 | range_check( 172 | "update_interval", 173 | config["AVELLANEDA_STOIKOV"].getint("update_interval"), 174 | 1, 175 | 6000, 176 | ) 177 | range_check("n_orders", config["AVELLANEDA_STOIKOV"].getint("n_orders"), 1, 19) 178 | range_check( 179 | "first_order_delta", 180 | config["AVELLANEDA_STOIKOV"].getfloat("first_order_delta"), 181 | 0, 182 | 0.20, 183 | ) 184 | range_check( 185 | "last_order_delta", 186 | config["AVELLANEDA_STOIKOV"].getfloat("last_order_delta"), 187 | 0.01, 188 | 0.03, 189 | ) 190 | range_check( 191 | "ask_total_asset_allocation", 192 | config["AVELLANEDA_STOIKOV"].getfloat("ask_total_asset_allocation"), 193 | 0.05, 194 | 0.60, 195 | ) 196 | range_check( 197 | "bid_total_asset_allocation", 198 | config["AVELLANEDA_STOIKOV"].getfloat("bid_total_asset_allocation"), 199 | 0.05, 200 | 0.60, 201 | ) 202 | range_check( 203 | "first_asset_allocation", 204 | config["AVELLANEDA_STOIKOV"].getfloat("first_asset_allocation"), 205 | 0, 206 | 0.05, 207 | ) 208 | range_check( 209 | "last_asset_allocation", 210 | config["AVELLANEDA_STOIKOV"].getfloat("last_asset_allocation"), 211 | 0.9, 212 | 1.2, 213 | ) 214 | --------------------------------------------------------------------------------