├── .flake8 ├── .github ├── scripts │ └── create_release.py └── workflows │ ├── docker-image.yml │ ├── greetings.yml │ └── python-publish.yml ├── .gitignore ├── .readthedocs.yaml ├── LICENSE ├── MANIFEST.in ├── README.md ├── app.py ├── back ├── ejtraderCT ├── __init__.py └── api │ ├── Symbol.py │ ├── __init__.py │ ├── buffer.py │ ├── ctrader.py │ ├── fix.py │ └── math.py ├── examples └── restapi │ ├── .env_example │ ├── Dockerfile │ ├── EjtraderCT RestAPI.postman_collection.json │ ├── README.md │ ├── app.py │ ├── docker-compose.yml │ └── requirements.txt ├── mkdocs.yml ├── pylintrc ├── pylintrcconda ├── requirements.txt ├── setup.cfg ├── setup.py └── test ├── fixTraderTest.py ├── symbol_table.py └── test_math.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 110 3 | -------------------------------------------------------------------------------- /.github/scripts/create_release.py: -------------------------------------------------------------------------------- 1 | from github import Github 2 | import os 3 | import re 4 | 5 | # Caminho para o diretório atual do script 6 | script_dir = os.path.dirname(os.path.realpath(__file__)) 7 | 8 | # Caminho para o setup.py relativo ao diretório do script 9 | setup_path = os.path.join(script_dir, "../../setup.py") 10 | 11 | # Leitura da versão do setup.py 12 | with open(setup_path, "r") as f: 13 | setup_contents = f.read() 14 | 15 | match = re.search(r"# Version: ([^\n]+)", setup_contents) 16 | if match: 17 | setup_version = match.group(1) 18 | else: 19 | raise ValueError("Could not find version in setup.py") 20 | 21 | # Acessa o repositório atual 22 | g = Github(os.getenv("GITHUB_TOKEN")) 23 | repo = g.get_repo(os.getenv("GITHUB_REPOSITORY")) 24 | 25 | # Busca a última release 26 | releases = repo.get_releases() 27 | latest_release = next((release for release in releases), None) 28 | 29 | # Se a versão do setup.py for diferente da última release, cria uma nova release 30 | if latest_release is None or latest_release.tag_name != setup_version: 31 | repo.create_git_ref(ref=f"refs/tags/{setup_version}", sha=os.getenv("GITHUB_SHA")) 32 | repo.create_git_release( 33 | tag=setup_version, 34 | name=f"Release {setup_version}", 35 | message="", 36 | draft=False, 37 | prerelease=False, 38 | target_commitish=os.getenv("GITHUB_SHA"), 39 | ) 40 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | paths: 6 | - "examples/**" 7 | 8 | jobs: 9 | docker: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Set up QEMU 13 | uses: docker/setup-qemu-action@v2 14 | - name: Set up Docker Buildx 15 | uses: docker/setup-buildx-action@v2 16 | - name: Login to DockerHub 17 | uses: docker/login-action@v2 18 | with: 19 | username: ${{ secrets.DOCKERHUB_USER }} 20 | password: ${{ secrets.DOCKERHUB_TOKEN }} 21 | - name: Build and push 22 | uses: docker/build-push-action@v3 23 | with: 24 | file: ./examples/restapi/Dockerfile 25 | platforms: linux/amd64,linux/arm64 26 | push: true 27 | tags: ejtrader/ejtraderct:rest 28 | -------------------------------------------------------------------------------- /.github/workflows/greetings.yml: -------------------------------------------------------------------------------- 1 | name: Greetings 2 | 3 | on: [pull_request_target, issues] 4 | 5 | jobs: 6 | greeting: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | steps: 12 | - uses: actions/first-interaction@v1 13 | with: 14 | repo-token: ${{ secrets.GITHUB_TOKEN }} 15 | issue-message: "We're glad you've opened your first issue. Please provide all the necessary details and any relevant code or screenshots to help us understand the problem better. Our team will review your issue and provide assistance as soon as possible. Thank you for contributing!" 16 | pr-message: "Congratulations on your first pull request! We appreciate your effort in contributing to the repository. Please ensure that your pull request includes a clear description of the changes you've made and any related issue numbers. Our team will review your pull request and provide feedback. Thank you for your valuable contribution!" 17 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | name: Create Release and Upload Python Package 2 | 3 | on: 4 | push: 5 | paths: 6 | - "setup.py" 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check out code 13 | uses: actions/checkout@v2 14 | 15 | - name: Set up Python 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: 3.9 19 | 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install PyGithub 24 | pip install twine 25 | pip install build 26 | 27 | - name: Create Release 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | run: python .github/scripts/create_release.py 31 | 32 | - name: Build package 33 | run: python -m build 34 | 35 | - name: Check if version exists 36 | run: | 37 | python -m twine check dist/* 38 | if python -m twine check dist/* | grep "already exists"; then 39 | echo "Version already exists, skipping publish step" 40 | exit 0 41 | fi 42 | 43 | - name: Publish package 44 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 45 | with: 46 | user: __token__ 47 | password: ${{ secrets.PYPI_API_TOKEN }} 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | build/ 3 | dist/ 4 | *.egg-info/ 5 | .vscode/ 6 | __pycache__ 7 | .DS_Store 8 | fix.ipynb 9 | examples/test 10 | .app.py 11 | .env -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | mkdocs: 9 | configuration: mkdocs.yml 10 | 11 | # Optionally build your docs in additional formats such as PDF 12 | formats: 13 | - pdf 14 | 15 | # Optionally set the version of Python and requirements required to build your docs 16 | python: 17 | version: 3.7 18 | install: 19 | - requirements: docs/requirements.txt 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 ejtrader.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include ejtraderCT/api/* 3 | include requirements.txt 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Pypi Publish](https://github.com/ejtraderLabs/ejtraderCT/actions/workflows/python-publish.yml/badge.svg) 2 | ![GitHub release (latest by date)](https://img.shields.io/github/v/release/ejtraderLabs/ejtraderCT) 3 | [![License](https://img.shields.io/github/license/ejtraderLabs/ejtraderCT)](https://github.com/ejtraderLabs/ejtraderCT/blob/master/LICENSE) 4 | [![PyPi downloads](https://img.shields.io/pypi/dm/ejtraderCT?style=flat-square&logo=pypi&logoColor=white)](https://pypi.org/project/ejtraderCT/) 5 | 6 | # Python Ctrader Fix API 7 | 8 | `ejtraderCT` is a Python library to access the Ctrader trading platform's FIX API. 9 | 10 | ## Features 11 | 12 | - [x] Market Position buy and sell 13 | - [x] Pending orders limit and stop (No SL and TP) 14 | - [x] Partial close (No SL and TP) 15 | - [x] Stop loss & Take profit Only Position 16 | - [x] Real-time bid & ask 17 | - [x] Check connection status 18 | - [x] Custom Client ID and commend in Position 19 | - [ ] Rest API server (in development) 20 | - [ ] Webhook for Tradingviewer (in development) 21 | 22 | ## Prerequisites 23 | 24 | The library has been tested on Python 3.7 to 3.9. 25 | 26 | ## Installation 27 | 28 | To install the latest version of `ejtraderCT`, you can use pip: 29 | 30 | ```shell 31 | pip install ejtraderCT -U 32 | ``` 33 | Or if you want to install from source, you can use: 34 | ```shell 35 | pip install git+https://github.com/ejtraderLabs/ejtraderCT.git 36 | ``` 37 | 38 | ## Accessing the Ctrader FIX API 39 | 40 | 41 | To access your API, follow these simple steps: 42 | 43 | 1. Open the cTrader desktop or web platform. 44 | 2. In the bottom left corner of the platform, you will find the **Settings** option. Click on it. 45 | 3. A popup window will appear. In the menu of the popup, look for the last option: **FIX API**. 46 | 4. First, click on the **Change Password** button. Make sure to add a numeric password of at least 8 digits. 47 | 5. After changing the password, click on the **Copy to Clipboard** button from **Trade Connection**. 48 | 6. Now, let's move to the **Trade Connection** section. Here, you will receive your data in the following format (this is an example with IC Markets for a real account): 49 | 50 | - Host name: (Current IP address 168.205.95.20 can be changed without notice) 51 | - Port: 5212 (SSL), 5202 (Plain text) 52 | - Password: (a/c 1104928 password) 53 | - SenderCompID: live.icmarkets.1104926 or demo.icmarkets.1104926 or live2.icmarkets.1104926 54 | - TargetCompID: cServer 55 | - SenderSubID: TRADE 56 | 57 | 58 | 59 | 60 | 61 | ### Import libraries 62 | 63 | ```python 64 | from ejtraderCT import Ctrader 65 | from time 66 | ``` 67 | 68 | ### Fix account login and details 69 | 70 | ```python 71 | server="168.205.95.20" # - Host name: (Current IP address 168.205.95.20 can be changed without notice) 72 | account="live.icmarkets.1104926" # - SenderCompID: live.icmarkets.1104926 73 | password="12345678" # - The password you configured 74 | 75 | api = Ctrader(server,account,password) 76 | time.sleep(3) 77 | 78 | ``` 79 | ##### Check the connection status 80 | ```python 81 | checkConnection = api.isconnected() 82 | print("Is Connected?: ", checkConnection) 83 | time.sleep(1) 84 | ``` 85 | ##### Logout 86 | ```python 87 | api.logout() 88 | ``` 89 | 90 | #### Real-time quote 91 | 92 | ##### Subscribe to symbols 93 | ```python 94 | api.subscribe("EURUSD", "GBPUSD") 95 | ``` 96 | ##### List of quotes for all symbols 97 | ```python 98 | quote = api.quote() 99 | print(quote) 100 | 101 | # Output 102 | 103 | {'EURUSD': {'bid': 1.02616, 'ask': 1.02618}, 'GBPUSD': {'bid': 1.21358, 'ask': 1.21362}} 104 | ``` 105 | 106 | #### Quote for a single symbol 107 | ```python 108 | quote = api.quote("EURUSD") 109 | print(quote) 110 | 111 | # Output 112 | 113 | {'bid': 1.02612, 'ask': 1.02614} 114 | 115 | ``` 116 | ### Market position and pending orders. 117 | 118 | ##### Market position 119 | 120 | ```python 121 | # Buy position 122 | price = api.quote() 123 | price = price['EURUSD']['bid'] 124 | 125 | symbol = "EURUSD" 126 | volume = 0.01 # position size: 127 | stoploss = round(price - 0.00010,6) 128 | takeprofit = round(price + 0.00010,6) 129 | 130 | id = api.buy(symbol, volume, stoploss, takeprofit) 131 | print(f"Position: {id}") 132 | 133 | # sell position 134 | price = api.quote() 135 | price = price['EURUSD']['bid'] 136 | 137 | symbol = "EURUSD" 138 | volume = 0.01 # position size 139 | stoploss = round(price + 0.00010,6) 140 | takeprofit = round(price - 0.00010,6) 141 | 142 | id = api.sell(symbol, volume, stoploss, takeprofit) 143 | print(f"Position: {id}") 144 | ``` 145 | 146 | ##### Limit Orders 147 | 148 | ```python 149 | 150 | # Buy limit order 151 | 152 | symbol = "EURUSD" 153 | volume = 0.01 # order size 154 | price = 1.18 # entry price 155 | 156 | id = api.buyLimit(symbol, volume, price) 157 | print(f"Order: {id}") 158 | 159 | 160 | # Sell limit order 161 | 162 | symbol = "EURUSD" 163 | volume = 0.01 # Order size 164 | price = 1.22 # entry price 165 | 166 | id = api.sellLimit(symbol, volume, price) 167 | print(f"Order: {id}") 168 | ``` 169 | 170 | #### Stop Orders 171 | 172 | ```python 173 | 174 | # Buy stop order 175 | 176 | symbol = "EURUSD" 177 | volume = 0.01 # order size 178 | price = 1.22 # entry price 179 | 180 | id = api.buyStop(symbol, volume, price) 181 | print(f"Order: {id}") 182 | 183 | # Sell stop order 184 | 185 | symbol = "EURUSD" 186 | volume = 0.01 # order size 187 | price = 1.18 # entry price 188 | 189 | api.sellStop(symbol, volume, price) 190 | 191 | ``` 192 | 193 | #### List Positions 194 | 195 | ```python 196 | positions = api.positions() 197 | print(positions) 198 | 199 | ``` 200 | #### List limit and stop Orders 201 | 202 | ```python 203 | orders = api.orders() 204 | print(orders) 205 | 206 | ``` 207 | #### Cancel order by id 208 | 209 | ```python 210 | orders = api.orders() 211 | for order in orders: 212 | api.orderCancelById(order['ord_id']) 213 | 214 | ``` 215 | #### Close position by id 216 | 217 | ```python 218 | for position in positions: 219 | api.positionCloseById(position['pos_id'], position['amount']) 220 | 221 | ``` 222 | 223 | #### Cancel all orders 224 | 225 | ```python 226 | api.cancel_all() 227 | ``` 228 | 229 | #### Close all positions 230 | 231 | ```python 232 | api.close_all() 233 | ``` 234 | #### Parcial Close position 235 | 236 | ```python 237 | api.positionPartialClose(id, volume) 238 | ``` 239 | 240 | ### Disclosure 241 | 242 | Due to certain limitations of the FIX API, there's a specific issue that arises when both the Stop Loss (SL) and Take Profit (TP) features are used concurrently. This issue occurs when one of them is triggered, the other remains open and will execute when the price reaches the specified level again, causing it to open another order. This issue needs to be addressed either within the ejtraderCT library or the application itself. 243 | 244 | However, you can avoid this problem by using either the SL or TP, but not both simultaneously. 245 | 246 | ## Contributing 247 | 248 | We welcome any contribution to `ejtraderCT`. Here are some ways to contribute: 249 | 250 | 1. Report issues or suggest improvements by opening an [issue](https://github.com/ejtraderLabs/ejtraderCT/issues). 251 | 2. Contribute with code to fix issues or add features via a [Pull Request](https://github.com/ejtraderLabs/ejtraderCT/pulls). 252 | 253 | Before submitting a pull request, please make sure your codes are well formatted and tested. 254 | 255 | ## Acknowledgements 256 | 257 | I would like to express my gratitude to [@HarukaMa](https://github.com/HarukaMa) for creating the initial project. Their work has been an invaluable starting point for my modifications and improvements. 258 | 259 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import List 3 | from pydantic import BaseModel 4 | from fastapi import FastAPI 5 | from ejtraderCT import Ctrader 6 | 7 | 8 | app = FastAPI() 9 | api = None 10 | 11 | 12 | class LoginModel(BaseModel): 13 | server: str 14 | account: str 15 | password: str 16 | 17 | 18 | class SymbolModel(BaseModel): 19 | symbols: List[str] 20 | 21 | 22 | class OrderModel(BaseModel): 23 | symbol: str 24 | volume: float 25 | stoploss: float 26 | takeprofit: float 27 | price: float = None 28 | 29 | 30 | class ModifyModel(BaseModel): 31 | id: str 32 | stoploss: float 33 | takeprofit: float 34 | price: float = None 35 | 36 | 37 | class IdModel(BaseModel): 38 | id: str 39 | 40 | 41 | @app.post("/login") 42 | async def login(): 43 | global api 44 | api = Ctrader( 45 | os.getenv("HOST_NAME"), os.getenv("SENDER_COMPID"), os.getenv("PASSWORD") 46 | ) 47 | if api.isconnected(): 48 | return {"message": "Logged in"} 49 | else: 50 | return {"error": "Check your credencials"} 51 | 52 | 53 | @app.get("/quote/{symbol}") 54 | async def quote(symbol: str = None): 55 | if symbol: 56 | quote = api.quote(symbol) 57 | else: 58 | quote = api.quote() 59 | return quote 60 | 61 | 62 | @app.post("/buy") 63 | async def buy(order: OrderModel): 64 | return { 65 | "Position": api.buy( 66 | order.symbol, order.volume, order.stoploss, order.takeprofit 67 | ) 68 | } 69 | 70 | 71 | @app.post("/sell") 72 | async def sell(order: OrderModel): 73 | return { 74 | "Position": api.sell( 75 | order.symbol, order.volume, order.stoploss, order.takeprofit 76 | ) 77 | } 78 | 79 | 80 | @app.post("/buyLimit") 81 | async def buy_limit(order: OrderModel): 82 | return { 83 | "Order": api.buyLimit( 84 | order.symbol, order.volume, order.stoploss, order.takeprofit, order.price 85 | ) 86 | } 87 | 88 | 89 | @app.post("/sellLimit") 90 | async def sell_limit(order: OrderModel): 91 | return { 92 | "Order": api.sellLimit( 93 | order.symbol, order.volume, order.stoploss, order.takeprofit, order.price 94 | ) 95 | } 96 | 97 | 98 | @app.post("/buyStop") 99 | async def buy_stop(order: OrderModel): 100 | return { 101 | "Order": api.buyStop( 102 | order.symbol, order.volume, order.stoploss, order.takeprofit, order.price 103 | ) 104 | } 105 | 106 | 107 | @app.post("/sellStop") 108 | async def sell_stop(order: OrderModel): 109 | return { 110 | "Order": api.sellStop( 111 | order.symbol, order.volume, order.stoploss, order.takeprofit, order.price 112 | ) 113 | } 114 | 115 | 116 | @app.get("/positions") 117 | async def positions(): 118 | return api.positions() 119 | 120 | 121 | @app.get("/orders") 122 | async def orders(): 123 | return api.orders() 124 | 125 | 126 | @app.post("/orderCancelById") 127 | async def order_cancel_by_id(id_model: IdModel): 128 | api.orderCancelById(id_model.id) 129 | return {"message": "Order cancelled"} 130 | 131 | 132 | @app.post("/positionCloseById") 133 | async def position_close_by_id(id_model: IdModel): 134 | api.positionCloseById(id_model.id) 135 | return {"message": "Position closed"} 136 | 137 | 138 | @app.post("/cancel_all") 139 | async def cancel_all(): 140 | api.cancel_all() 141 | return {"message": "All orders cancelled"} 142 | 143 | 144 | @app.post("/close_all") 145 | async def close_all(): 146 | api.close_all() 147 | return {"message": "All positions closed"} 148 | 149 | 150 | @app.post("/positionModify") 151 | async def position_modify(modify: ModifyModel): 152 | api.positionModify(modify.id, modify.stoploss, modify.takeprofit) 153 | return {"message": "Position modified"} 154 | 155 | 156 | @app.post("/orderModify") 157 | async def order_modify(modify: ModifyModel): 158 | api.orderModify(modify.id, modify.stoploss, modify.takeprofit, modify.price) 159 | return {"message": "Order modified"} 160 | 161 | 162 | @app.post("/logout") 163 | async def logout(): 164 | return {"message": api.logout()} 165 | 166 | 167 | @app.post("/status") 168 | async def status(): 169 | return {"connected": api.isconnected()} 170 | -------------------------------------------------------------------------------- /back: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "ejtraderCT" 3 | version = "1.1.1rc0" 4 | description = "Ctrader Fix API" 5 | authors = ["Emerson Pedroso "] 6 | homepage = "https://github.com/ejtraderLabs/ejtraderCT" 7 | documentation = "https://github.com/ejtraderLabs/ejtraderCT/wiki" 8 | readme = "README.md" 9 | license = "MIT" 10 | classifiers = [ 11 | 'License :: OSI Approved :: MIT License', 12 | 'Natural Language :: English', 13 | 'Programming Language :: Python :: 3.8', 14 | ] 15 | include = [ 16 | "README.md", 17 | "ejtraderCT/api/*", 18 | "requirements.txt", 19 | "LICENSE" 20 | ] 21 | 22 | [tool.poetry.dependencies] 23 | python = "^3.8" 24 | 25 | [tool.poetry.dev-dependencies] 26 | 27 | [tool.black] 28 | line-length=100 29 | 30 | [tool.pylint.reports] 31 | output-format="colorized" 32 | reports="y" 33 | include-ids="yes" 34 | msg-template="{msg_id}:{line:3d},{column}: {obj}: {msg}" 35 | 36 | [tool.pytest.ini_options] 37 | addopts = "--cov=ejtraderCT --cov-branch --cov-report term-missing -vv --color=yes --cov-fail-under 100" 38 | python_files = "tests.py test_*.py *_tests.py" 39 | 40 | [build-system] 41 | requires = ["poetry>=0.12"] 42 | build-backend = "poetry.masonry.api" 43 | -------------------------------------------------------------------------------- /ejtraderCT/__init__.py: -------------------------------------------------------------------------------- 1 | from .api.ctrader import Ctrader #Noa 2 | -------------------------------------------------------------------------------- /ejtraderCT/api/Symbol.py: -------------------------------------------------------------------------------- 1 | SYMBOLSLIST = { 2 | 'default': { 3 | 1: {'id': 1, 'pip_position': 4, 'name': 'EURUSD', 'bid_volume': 0, 'ask_volume': 0}, 4 | 2: {'id': 2, 'pip_position': 4, 'name': 'GBPUSD', 'bid_volume': 0, 'ask_volume': 0}, 5 | 3: {'id': 3, 'pip_position': 2, 'name': 'EURJPY', 'bid_volume': 0, 'ask_volume': 0}, 6 | 4: {'id': 4, 'pip_position': 2, 'name': 'USDJPY', 'bid_volume': 0, 'ask_volume': 0}, 7 | 5: {'id': 5, 'pip_position': 4, 'name': 'AUDUSD', 'bid_volume': 0, 'ask_volume': 0}, 8 | 6: {'id': 6, 'pip_position': 4, 'name': 'USDCHF', 'bid_volume': 0, 'ask_volume': 0}, 9 | 7: {'id': 7, 'pip_position': 2, 'name': 'GBPJPY', 'bid_volume': 0, 'ask_volume': 0}, 10 | 8: {'id': 8, 'pip_position': 4, 'name': 'USDCAD', 'bid_volume': 0, 'ask_volume': 0}, 11 | 9: {'id': 9, 'pip_position': 4, 'name': 'EURGBP', 'bid_volume': 0, 'ask_volume': 0}, 12 | 10: {'id': 10, 'pip_position': 4, 'name': 'EURCHF', 'bid_volume': 0, 'ask_volume': 0}, 13 | 11: {'id': 11, 'pip_position': 2, 'name': 'AUDJPY', 'bid_volume': 0, 'ask_volume': 0}, 14 | 12: {'id': 12, 'pip_position': 4, 'name': 'NZDUSD', 'bid_volume': 0, 'ask_volume': 0}, 15 | 13: {'id': 13, 'pip_position': 2, 'name': 'CHFJPY', 'bid_volume': 0, 'ask_volume': 0}, 16 | 14: {'id': 14, 'pip_position': 4, 'name': 'EURAUD', 'bid_volume': 0, 'ask_volume': 0}, 17 | 15: {'id': 15, 'pip_position': 2, 'name': 'CADJPY', 'bid_volume': 0, 'ask_volume': 0}, 18 | 16: {'id': 16, 'pip_position': 4, 'name': 'GBPAUD', 'bid_volume': 0, 'ask_volume': 0}, 19 | 17: {'id': 17, 'pip_position': 4, 'name': 'EURCAD', 'bid_volume': 0, 'ask_volume': 0}, 20 | 18: {'id': 18, 'pip_position': 4, 'name': 'AUDCAD', 'bid_volume': 0, 'ask_volume': 0}, 21 | 19: {'id': 19, 'pip_position': 4, 'name': 'GBPCAD', 'bid_volume': 0, 'ask_volume': 0}, 22 | 20: {'id': 20, 'pip_position': 4, 'name': 'AUDNZD', 'bid_volume': 0, 'ask_volume': 0}, 23 | 21: {'id': 21, 'pip_position': 2, 'name': 'NZDJPY', 'bid_volume': 0, 'ask_volume': 0}, 24 | 22: {'id': 22, 'pip_position': 4, 'name': 'USDNOK', 'bid_volume': 0, 'ask_volume': 0}, 25 | 23: {'id': 23, 'pip_position': 4, 'name': 'AUDCHF', 'bid_volume': 0, 'ask_volume': 0}, 26 | 24: {'id': 24, 'pip_position': 4, 'name': 'USDMXN', 'bid_volume': 0, 'ask_volume': 0}, 27 | 25: {'id': 25, 'pip_position': 4, 'name': 'GBPNZD', 'bid_volume': 0, 'ask_volume': 0}, 28 | 26: {'id': 26, 'pip_position': 4, 'name': 'EURNZD', 'bid_volume': 0, 'ask_volume': 0}, 29 | 27: {'id': 27, 'pip_position': 4, 'name': 'CADCHF', 'bid_volume': 0, 'ask_volume': 0}, 30 | 28: {'id': 28, 'pip_position': 4, 'name': 'USDSGD', 'bid_volume': 0, 'ask_volume': 0}, 31 | 29: {'id': 29, 'pip_position': 4, 'name': 'USDSEK', 'bid_volume': 0, 'ask_volume': 0}, 32 | 30: {'id': 30, 'pip_position': 4, 'name': 'NZDCAD', 'bid_volume': 0, 'ask_volume': 0}, 33 | 31: {'id': 31, 'pip_position': 4, 'name': 'EURSEK', 'bid_volume': 0, 'ask_volume': 0}, 34 | 32: {'id': 32, 'pip_position': 4, 'name': 'GBPSGD', 'bid_volume': 0, 'ask_volume': 0}, 35 | 33: {'id': 33, 'pip_position': 4, 'name': 'EURNOK', 'bid_volume': 0, 'ask_volume': 0}, 36 | 34: {'id': 34, 'pip_position': 4, 'name': 'EURHUF', 'bid_volume': 0, 'ask_volume': 0}, 37 | 35: {'id': 35, 'pip_position': 4, 'name': 'USDPLN', 'bid_volume': 0, 'ask_volume': 0}, 38 | 36: {'id': 36, 'pip_position': 4, 'name': 'USDDKK', 'bid_volume': 0, 'ask_volume': 0}, 39 | 37: {'id': 37, 'pip_position': 4, 'name': 'GBPNOK', 'bid_volume': 0, 'ask_volume': 0}, 40 | 39: {'id': 39, 'pip_position': 4, 'name': 'NZDCHF', 'bid_volume': 0, 'ask_volume': 0}, 41 | 40: {'id': 40, 'pip_position': 4, 'name': 'GBPCHF', 'bid_volume': 0, 'ask_volume': 0}, 42 | 43: {'id': 43, 'pip_position': 4, 'name': 'USDTRY', 'bid_volume': 0, 'ask_volume': 0}, 43 | 44: {'id': 44, 'pip_position': 4, 'name': 'EURTRY', 'bid_volume': 0, 'ask_volume': 0}, 44 | 46: {'id': 46, 'pip_position': 4, 'name': 'EURZAR', 'bid_volume': 0, 'ask_volume': 0}, 45 | 47: {'id': 47, 'pip_position': 2, 'name': 'SGDJPY', 'bid_volume': 0, 'ask_volume': 0}, 46 | 48: {'id': 48, 'pip_position': 4, 'name': 'USDHKD', 'bid_volume': 0, 'ask_volume': 0}, 47 | 49: {'id': 49, 'pip_position': 4, 'name': 'USDZAR', 'bid_volume': 0, 'ask_volume': 0}, 48 | 50: {'id': 50, 'pip_position': 4, 'name': 'EURMXN', 'bid_volume': 0, 'ask_volume': 0}, 49 | 51: {'id': 51, 'pip_position': 4, 'name': 'EURPLN', 'bid_volume': 0, 'ask_volume': 0}, 50 | 53: {'id': 53, 'pip_position': 4, 'name': 'NZDSGD', 'bid_volume': 0, 'ask_volume': 0}, 51 | 54: {'id': 54, 'pip_position': 4, 'name': 'USDHUF', 'bid_volume': 0, 'ask_volume': 0}, 52 | 55: {'id': 55, 'pip_position': 4, 'name': 'EURCZK', 'bid_volume': 0, 'ask_volume': 0}, 53 | 56: {'id': 56, 'pip_position': 4, 'name': 'USDCZK', 'bid_volume': 0, 'ask_volume': 0}, 54 | 57: {'id': 57, 'pip_position': 4, 'name': 'EURDKK', 'bid_volume': 0, 'ask_volume': 0}, 55 | 60: {'id': 60, 'pip_position': 4, 'name': 'USDCNH', 'bid_volume': 0, 'ask_volume': 0}, 56 | 61: {'id': 61, 'pip_position': 4, 'name': 'GBPSEK', 'bid_volume': 0, 'ask_volume': 0}, 57 | }, 58 | 'mpa': { 59 | 99: {'id': 99, 'pip_position': 4, 'name': '99', 'bid_volume': 0, 'ask_volume': 0}, 60 | 100: {'id': 100, 'pip_position': 4, 'name': '100', 'bid_volume': 0, 'ask_volume': 0}, 61 | 101: {'id': 101, 'pip_position': 2, 'name': '101', 'bid_volume': 0, 'ask_volume': 0}, 62 | 102: {'id': 102, 'pip_position': 4, 'name': '102', 'bid_volume': 0, 'ask_volume': 0}, 63 | 103: {'id': 103, 'pip_position': 4, 'name': '103', 'bid_volume': 0, 'ask_volume': 0}, 64 | 104: {'id': 104, 'pip_position': 4, 'name': '104', 'bid_volume': 0, 'ask_volume': 0}, 65 | 105: {'id': 105, 'pip_position': 2, 'name': '105', 'bid_volume': 0, 'ask_volume': 0}, 66 | 106: {'id': 106, 'pip_position': 2, 'name': '106', 'bid_volume': 0, 'ask_volume': 0}, 67 | 107: {'id': 107, 'pip_position': 4, 'name': '107', 'bid_volume': 0, 'ask_volume': 0}, 68 | 108: {'id': 108, 'pip_position': 4, 'name': '108', 'bid_volume': 0, 'ask_volume': 0}, 69 | 109: {'id': 109, 'pip_position': 4, 'name': '109', 'bid_volume': 0, 'ask_volume': 0}, 70 | 110: {'id': 110, 'pip_position': 4, 'name': '110', 'bid_volume': 0, 'ask_volume': 0}, 71 | 111: {'id': 111, 'pip_position': 4, 'name': '111', 'bid_volume': 0, 'ask_volume': 0}, 72 | 112: {'id': 112, 'pip_position': 4, 'name': '112', 'bid_volume': 0, 'ask_volume': 0}, 73 | 113: {'id': 113, 'pip_position': 2, 'name': '113', 'bid_volume': 0, 'ask_volume': 0}, 74 | 114: {'id': 114, 'pip_position': 4, 'name': '114', 'bid_volume': 0, 'ask_volume': 0}, 75 | 115: {'id': 115, 'pip_position': 4, 'name': '115', 'bid_volume': 0, 'ask_volume': 0}, 76 | 116: {'id': 116, 'pip_position': 4, 'name': '116', 'bid_volume': 0, 'ask_volume': 0}, 77 | 117: {'id': 117, 'pip_position': 4, 'name': '117', 'bid_volume': 0, 'ask_volume': 0}, 78 | 118: {'id': 118, 'pip_position': 4, 'name': '118', 'bid_volume': 0, 'ask_volume': 0}, 79 | 119: {'id': 119, 'pip_position': 4, 'name': '119', 'bid_volume': 0, 'ask_volume': 0}, 80 | 120: {'id': 120, 'pip_position': 4, 'name': '120', 'bid_volume': 0, 'ask_volume': 0}, 81 | 121: {'id': 121, 'pip_position': 4, 'name': '121', 'bid_volume': 0, 'ask_volume': 0}, 82 | 122: {'id': 122, 'pip_position': 4, 'name': '122', 'bid_volume': 0, 'ask_volume': 0}, 83 | 123: {'id': 123, 'pip_position': 2, 'name': '123', 'bid_volume': 0, 'ask_volume': 0}, 84 | 124: {'id': 124, 'pip_position': 4, 'name': '124', 'bid_volume': 0, 'ask_volume': 0}, 85 | 125: {'id': 125, 'pip_position': 4, 'name': '125', 'bid_volume': 0, 'ask_volume': 0}, 86 | 126: {'id': 126, 'pip_position': 4, 'name': '126', 'bid_volume': 0, 'ask_volume': 0}, 87 | 127: {'id': 127, 'pip_position': 2, 'name': '127', 'bid_volume': 0, 'ask_volume': 0}, 88 | 128: {'id': 128, 'pip_position': 2, 'name': '128', 'bid_volume': 0, 'ask_volume': 0}, 89 | 129: {'id': 129, 'pip_position': 4, 'name': '129', 'bid_volume': 0, 'ask_volume': 0}, 90 | 130: {'id': 130, 'pip_position': 4, 'name': '130', 'bid_volume': 0, 'ask_volume': 0}, 91 | 131: {'id': 131, 'pip_position': 4, 'name': '131', 'bid_volume': 0, 'ask_volume': 0}, 92 | 132: {'id': 132, 'pip_position': 2, 'name': '132', 'bid_volume': 0, 'ask_volume': 0}, 93 | 133: {'id': 133, 'pip_position': 4, 'name': '133', 'bid_volume': 0, 'ask_volume': 0}, 94 | 134: {'id': 134, 'pip_position': 4, 'name': '134', 'bid_volume': 0, 'ask_volume': 0}, 95 | 135: {'id': 135, 'pip_position': 2, 'name': '135', 'bid_volume': 0, 'ask_volume': 0}, 96 | 136: {'id': 136, 'pip_position': 4, 'name': '136', 'bid_volume': 0, 'ask_volume': 0}, 97 | 137: {'id': 137, 'pip_position': 4, 'name': '137', 'bid_volume': 0, 'ask_volume': 0}, 98 | 138: {'id': 138, 'pip_position': 4, 'name': '138', 'bid_volume': 0, 'ask_volume': 0}, 99 | 139: {'id': 139, 'pip_position': 4, 'name': '139', 'bid_volume': 0, 'ask_volume': 0}, 100 | 140: {'id': 140, 'pip_position': 4, 'name': '140', 'bid_volume': 0, 'ask_volume': 0}, 101 | 141: {'id': 141, 'pip_position': 2, 'name': '141', 'bid_volume': 0, 'ask_volume': 0}, 102 | 142: {'id': 142, 'pip_position': 4, 'name': '142', 'bid_volume': 0, 'ask_volume': 0}, 103 | 143: {'id': 143, 'pip_position': 4, 'name': '143', 'bid_volume': 0, 'ask_volume': 0}, 104 | 144: {'id': 144, 'pip_position': 4, 'name': '144', 'bid_volume': 0, 'ask_volume': 0}, 105 | 145: {'id': 145, 'pip_position': 4, 'name': '145', 'bid_volume': 0, 'ask_volume': 0}, 106 | 146: {'id': 146, 'pip_position': 4, 'name': '146', 'bid_volume': 0, 'ask_volume': 0}, 107 | 147: {'id': 147, 'pip_position': 4, 'name': '147', 'bid_volume': 0, 'ask_volume': 0}, 108 | 148: {'id': 148, 'pip_position': 4, 'name': '148', 'bid_volume': 0, 'ask_volume': 0}, 109 | 206: {'id': 206, 'pip_position': 4, 'name': '206', 'bid_volume': 0, 'ask_volume': 0}, 110 | } 111 | } -------------------------------------------------------------------------------- /ejtraderCT/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ejtraderLabs/ejtraderCT/2358ca90bad156a9d82e38a06ff21c7d57d50b8c/ejtraderCT/api/__init__.py -------------------------------------------------------------------------------- /ejtraderCT/api/buffer.py: -------------------------------------------------------------------------------- 1 | 2 | class Buffer: 3 | def __init__(self): 4 | self._buffer = bytearray() 5 | 6 | def write(self, data: bytes): 7 | self._buffer.extend(data) 8 | 9 | def read(self, size: int): 10 | data = self._buffer[:size] 11 | self._buffer[:size] = b"" 12 | return data 13 | 14 | def peek(self, size: int): 15 | return self._buffer[:size] 16 | 17 | def count(self): 18 | return len(self._buffer) 19 | 20 | def __len__(self): 21 | return len(self._buffer) -------------------------------------------------------------------------------- /ejtraderCT/api/ctrader.py: -------------------------------------------------------------------------------- 1 | # from locale import currency 2 | import logging 3 | import json 4 | import time 5 | import random 6 | from operator import itemgetter 7 | from .fix import FIX, Side, OrderType 8 | from .Symbol import SYMBOLSLIST 9 | 10 | 11 | class Ctrader: 12 | def __init__( 13 | self, 14 | server=None, 15 | account=None, 16 | password=None, 17 | currency="EUR", 18 | client_id=1, 19 | spread=0.00005, 20 | debug=False, 21 | ): 22 | """AI is creating summary for __init__ 23 | 24 | Args: 25 | server ([str]): [example h8.p.c-trader.cn] 26 | account ([str]): [live.icmarkets.1104926 or demo.icmarkets.1104926] 27 | client_id ([str]):[example 1 or trader-1 its comment on position label] 28 | spread ([int]): [example 0.00010 default 0.00005] 29 | password ([str]): [example 12345678 need to setup when you create api on ctrader platform] 30 | """ 31 | if debug: 32 | logging.getLogger().setLevel(logging.INFO) 33 | split_string = account.split(".") 34 | broker = split_string[0] + "." + split_string[1] 35 | login = split_string[2] 36 | 37 | self.client = c = { 38 | "_id": client_id, 39 | "server": server, 40 | "broker": broker, 41 | "login": login, 42 | "password": password, 43 | "currency": currency, 44 | "fix_status": 0, 45 | "positions": [], 46 | "orders": [], 47 | } 48 | self.fix = FIX( 49 | c["server"], 50 | c["broker"], 51 | c["login"], 52 | c["password"], 53 | c["currency"], 54 | c["_id"], 55 | self.position_list_callback, 56 | self.order_list_callback, 57 | ) 58 | self.market_data_list = {} 59 | self.symbol_table = SYMBOLSLIST["default"] 60 | 61 | def trade( 62 | self, 63 | symbol, 64 | action, 65 | type, 66 | actionType, 67 | volume, 68 | stoploss, 69 | takeprofit, 70 | price, 71 | deviation, 72 | id, 73 | ): 74 | v_action = action 75 | v_symbol = symbol 76 | v_ticket = ( 77 | id 78 | if id 79 | else "{:.7f}".format(time.time()).replace(".", "") 80 | + str(random.randint(10000, 99999)) 81 | ) 82 | v_type = str(type) 83 | v_openprice = price 84 | v_lots = volume 85 | v_sl = stoploss 86 | v_tp = takeprofit 87 | 88 | logging.info( 89 | "Action: %s, Symbol: %s, Lots: %s, Ticket: %s, price: %s, takeprofit: %s, stoploss: %s, type: %s", 90 | v_action, 91 | v_symbol, 92 | v_lots, 93 | v_ticket, 94 | v_openprice, 95 | v_sl, 96 | v_tp, 97 | v_type, 98 | ) 99 | 100 | otype = actionType 101 | symbol = v_symbol[:6] 102 | size = int(float(v_lots) * 100000) 103 | global ticket 104 | ticket = None 105 | client_id = str(self.client["_id"]) 106 | command = "" 107 | if v_action == "OPEN": 108 | if int(v_type) > 1: 109 | # abre ordem pendente 110 | command = "{0} {1} {2} {3} {4}".format( 111 | otype, symbol, size, v_openprice, v_ticket 112 | ) 113 | self.parse_command(command, client_id) 114 | else: 115 | # abre posicao a mercado 116 | command = "{0} {1} {2} {3}".format(otype, symbol, size, v_ticket) 117 | self.parse_command(command, client_id) 118 | 119 | if v_sl or v_tp: 120 | while True: 121 | try: 122 | ticket = self.fix.origin_to_pos_id[v_ticket] 123 | if ticket: # Verifica se a variável ticket não está vazia 124 | break 125 | except Exception as e: 126 | logging.info(e) 127 | continue 128 | 129 | if ticket: 130 | if float(v_sl) > 0: 131 | # abre posicao pendente SL 132 | otype = "sell stop" if v_type == "0" else "buy stop" 133 | command = "{0} {1} {2} {3} {4} {5}".format( 134 | otype, symbol, size, v_sl, v_ticket, ticket 135 | ) 136 | self.parse_command(command, client_id) 137 | if float(v_tp) > 0: 138 | # cancela ordens pendentes abertas de TP e SL 139 | ticket_orders = self.getOrdersIdByOriginId(v_ticket, client_id) 140 | # abre posicao pendente TP 141 | otype = "sell limit" if v_type == "0" else "buy limit" 142 | command = "{0} {1} {2} {3} {4} {5}".format( 143 | otype, symbol, size, v_tp, v_ticket, ticket 144 | ) 145 | self.parse_command(command, client_id) 146 | 147 | elif v_action in ["CLOSED", "PCLOSED"]: 148 | if int(v_type) > 1: 149 | # ORDEM 150 | # cancela ordens pendentes 151 | self.fix.cancel_order(v_ticket) 152 | ticket_orders = self.getOrdersIdByOriginId(v_ticket, client_id) 153 | self.cancelOrdersByOriginId(ticket_orders, client_id) 154 | self.parse_command(command, client_id) 155 | return 156 | else: 157 | # POSICAO 158 | self.fix.close_position(v_ticket, size) 159 | # cancela ordens pendentes abertas de TP e SL 160 | ticket_orders = self.getOrdersIdByOriginId(v_ticket, client_id) 161 | self.cancelOrdersByOriginId(ticket_orders, client_id) 162 | self.parse_command(command, client_id) 163 | return 164 | 165 | return v_ticket 166 | 167 | def buy(self, symbol, volume, stoploss, takeprofit, price=0): 168 | """summary for buy 169 | 170 | Args: 171 | symbol ([str]): ["EURUSD"] 172 | volume ([float]): [0.01] 173 | stoploss ([float]): [1.18] 174 | takeprofit ([float]): [1.19] 175 | price (int, optional): [on the price]. Defaults to 0. 176 | Returns: 177 | [int]: [order ID] 178 | """ 179 | return self.trade( 180 | symbol, 181 | "OPEN", 182 | 0, 183 | "buy", 184 | volume, 185 | stoploss, 186 | takeprofit, 187 | price, 188 | None, 189 | None, 190 | ) 191 | 192 | def sell(self, symbol, volume, stoploss=0, takeprofit=9, price=0): 193 | """summary for sell 194 | 195 | Args: 196 | symbol ([str]): ["EURUSD"] 197 | volume ([float]): [0.01] 198 | stoploss ([float]): [1.19] 199 | takeprofit ([float]): [1.18] 200 | price (int, optional): [on the price]. Defaults to 0. 201 | Returns: 202 | [int]: [Order ID] 203 | """ 204 | return self.trade( 205 | symbol, 206 | "OPEN", 207 | 1, 208 | "sell", 209 | volume, 210 | stoploss, 211 | takeprofit, 212 | price, 213 | None, 214 | None, 215 | ) 216 | 217 | def buyLimit(self, symbol, volume, price=0): 218 | """summary for buy Limit 219 | 220 | Args: 221 | symbol ([str]): ["EURUSD"] 222 | volume ([float]): [0.01] 223 | price ([float]): [1.8]. Defaults to 0. 224 | Returns: 225 | [int]: [order ID] 226 | """ 227 | return self.trade( 228 | symbol, 229 | "OPEN", 230 | 2, 231 | "buy limit", 232 | volume, 233 | None, 234 | None, 235 | price, 236 | None, 237 | None, 238 | ) 239 | 240 | def sellLimit(self, symbol, volume, price=0): 241 | """summary for sellLimit 242 | 243 | Args: 244 | symbol ([str]): ["EURUSD"] 245 | volume ([float]): [0.01] 246 | price (int, optional): [1.22]. Defaults to 0. 247 | Returns: 248 | [type]: [description] 249 | """ 250 | return self.trade( 251 | symbol, 252 | "OPEN", 253 | 3, 254 | "sell limit", 255 | volume, 256 | None, 257 | None, 258 | price, 259 | None, 260 | None, 261 | ) 262 | 263 | def buyStop(self, symbol, volume, price=0): 264 | """summary for buyStop 265 | 266 | Args: 267 | symbol ([str]): ["EURUSD"] 268 | volume ([float]): [0.01] 269 | price (int, optional): [1.22]. Defaults to 0. 270 | Returns: 271 | [type]: [description] 272 | """ 273 | return self.trade( 274 | symbol, 275 | "OPEN", 276 | 4, 277 | "buy stop", 278 | volume, 279 | None, 280 | None, 281 | price, 282 | None, 283 | None, 284 | ) 285 | 286 | def sellStop(self, symbol, volume, price=0): 287 | """summary for sellStop 288 | 289 | Args: 290 | symbol ([str]): ["EURUSD"] 291 | volume ([float]): [0.01] 292 | price (int, optional): [1.22]. Defaults to 0. 293 | Returns: 294 | [type]: [description] 295 | """ 296 | return self.trade( 297 | symbol, 298 | "OPEN", 299 | 5, 300 | "sell stop", 301 | volume, 302 | None, 303 | None, 304 | price, 305 | None, 306 | None, 307 | ) 308 | 309 | def positionClosePartial(self, id, volume): 310 | return self.trade("", "PCLOSED", 0, "", volume, 0, 0, 0, 5, id) 311 | 312 | def positionCloseById(self, id, amount): 313 | try: 314 | action = self.trade("", "CLOSED", 0, "", amount / 100000, 0, 0, 0, 5, id) 315 | except Exception as e: 316 | logging.info(e) 317 | action = None 318 | pass 319 | return action 320 | 321 | def orderCancelById(self, id): 322 | try: 323 | action = self.trade("", "CLOSED", 2, "", 0, 0, 0, 0, 5, id) 324 | except Exception as e: 325 | logging.info(e) 326 | action = None 327 | pass 328 | return action 329 | 330 | def positions(self): 331 | return json.loads(json.dumps(self.client["positions"])) 332 | 333 | def orders(self): 334 | return json.loads(json.dumps(self.client["orders"])) 335 | 336 | def parse_command(self, command: str, client_id: str): 337 | parts = command.split(" ") 338 | logging.info(parts) 339 | logging.info(f"Command: {command} ") 340 | 341 | if not self.fix.logged: 342 | logging.info("waiting logging...") 343 | return 344 | 345 | if parts[0] == "sub": 346 | try: 347 | subid = int(parts[1]) 348 | self.fix.market_request( 349 | subid - 1, parts[2].upper(), self.quote_callback 350 | ) 351 | except ValueError: 352 | logging.error("Invalid subscription ID") 353 | if parts[0] in ["buy", "sell"]: 354 | if parts[1] in ["stop", "limit"]: 355 | self.fix.new_limit_order( 356 | parts[2].upper(), 357 | Side.Buy if parts[0] == "buy" else Side.Sell, 358 | OrderType.Limit if parts[1] == "limit" else OrderType.Stop, 359 | float(parts[3]), 360 | float(parts[4]), 361 | parts[5] if len(parts) >= 6 else None, 362 | parts[6] if len(parts) >= 7 else None, 363 | ) 364 | else: 365 | self.fix.new_market_order( 366 | parts[1].upper(), 367 | Side.Buy if parts[0] == "buy" else Side.Sell, 368 | float(parts[2]), 369 | parts[3] if len(parts) >= 4 else None, 370 | parts[4] if len(parts) >= 5 else None, 371 | ) 372 | if parts[0] == "close": 373 | if parts[1] == "all": 374 | self.fix.close_all() 375 | else: 376 | if len(parts) == 3: 377 | self.fix.close_position(parts[1], parts[2]) 378 | else: 379 | self.fix.close_position(parts[1]) 380 | if parts[0] == "cancel": 381 | if parts[1] == "all": 382 | self.fix.cancel_all() 383 | else: 384 | self.fix.cancel_order(parts[1]) 385 | 386 | def float_format(self, fmt: str, num: float, force_sign=True): 387 | return max( 388 | ("{:+}" if force_sign else "{}").format(round(num, 6)), 389 | fmt.format(num), 390 | key=len, 391 | ) 392 | 393 | def position_list_callback(self, data: dict, price_data: dict, client_id: str): 394 | positions = [] 395 | for i, kv in enumerate(data.items()): 396 | pos_id = kv[0] 397 | name = kv[1]["name"] 398 | side = "Buy" if kv[1]["long"] > 0 else "Sell" 399 | amount = kv[1]["long"] if kv[1]["long"] > 0 else kv[1]["short"] 400 | price_str = self.float_format( 401 | "{:.%df}" % kv[1]["digits"], kv[1]["price"], False 402 | ) 403 | price = price_data.get(name, None) 404 | actual_price = "" 405 | diff_str = "" 406 | pl_str = "" 407 | gain_str = "" 408 | if price: 409 | if side == "Buy": 410 | p = price["bid"] 411 | else: 412 | p = price["ask"] 413 | actual_price = ("{:.%df}" % kv[1]["digits"]).format(p) 414 | diff = p - kv[1]["price"] 415 | if side == "Sell": 416 | diff = -diff 417 | diff_str = self.float_format("{:+.%df}" % kv[1]["digits"], diff) 418 | pl = amount * diff 419 | pl_str = self.float_format("{:+.2f}", pl) 420 | convert = kv[1]["convert"] 421 | convert_dir = kv[1]["convert_dir"] 422 | price = price_data.get(convert, None) 423 | if price: 424 | if convert_dir: 425 | rate = 1 / price["ask"] 426 | else: 427 | rate = price["bid"] 428 | pl_base = pl * rate 429 | gain_str = "{:+.2f}".format(round(pl_base, 2)) 430 | # adiciona informacoes de posicoes no client 431 | positions.append( 432 | { 433 | "pos_id": pos_id, 434 | "name": name, 435 | "side": side, 436 | "amount": amount, 437 | "price": price_str, 438 | "actual_price": actual_price, 439 | "diff": diff_str, 440 | "pl": pl_str, 441 | "gain": gain_str, 442 | } 443 | ) 444 | self.client.update(positions=positions) 445 | logging.debug("client_id %s positions: %s", client_id, positions) 446 | 447 | def getPositionIdByOriginId(self, posId: str, client_id: str): 448 | if posId in self.fix.origin_to_pos_id: 449 | return self.fix.position_list[self.fix.origin_to_pos_id[posId]] 450 | 451 | def getOrdersIdByOriginId(self, ordId: str, client_id: str): 452 | if ( 453 | ordId in self.fix.origin_to_ord_id 454 | ): # Verifique se a chave existe antes de acessá-la 455 | return self.fix.origin_to_ord_id[ordId] 456 | else: 457 | return None # Retorne None ou outro valor padrão quando a chave não existir 458 | 459 | def cancelOrdersByOriginId(self, clIdArr, client_id: str): 460 | if not clIdArr: 461 | return 462 | for clId in clIdArr: 463 | self.fix.cancel_order(clId) 464 | 465 | def subscribe(self, *symbol): 466 | symbol = list(symbol) 467 | for symbols in symbol: 468 | self.fix.spot_market_request(symbols) 469 | 470 | def quote(self, symbol=None): 471 | if symbol and symbol not in self.fix.spot_price_list: 472 | return "Symbol not Subscribed" 473 | elif symbol: 474 | return self.fix.spot_price_list[symbol] 475 | return self.fix.spot_price_list 476 | 477 | def order_list_callback(self, data: dict, price_data: dict, client_id: str): 478 | orders = [] 479 | for i, kv in enumerate(data.items()): 480 | ord_id = kv[0] 481 | name = kv[1]["name"] 482 | side = "Buy" if kv[1]["side"] == Side.Buy else "Sell" 483 | amount = kv[1]["amount"] 484 | order_type = kv[1]["type"] 485 | price_str = "" 486 | if order_type > 1: 487 | price_str = self.float_format( 488 | "{:.%df}" % kv[1]["digits"], kv[1]["price"], False 489 | ) 490 | price = price_data.get(name, None) 491 | actual_price = "" 492 | if price: 493 | if side == "Buy": 494 | price = price["ask"] 495 | else: 496 | price = price["bid"] 497 | actual_price = self.float_format( 498 | "{:.%df}" % kv[1]["digits"], price, False 499 | ) 500 | pos_id = kv[1]["pos_id"] 501 | # adiciona informacoes de ordens no client 502 | orders.append( 503 | { 504 | "ord_id": ord_id, 505 | "name": name, 506 | "side": side, 507 | "amount": amount, 508 | "price": price_str, 509 | "actual_price": actual_price, 510 | "pos_id": pos_id, 511 | "clid": kv[1]["clid"], 512 | } 513 | ) 514 | self.client.update(orders=orders) 515 | logging.debug("client_id %s orders: %s", client_id, orders) 516 | 517 | def quote_callback(self, name: str, digits: int, data: dict): 518 | if len(data) == 0: 519 | return 520 | ask = [] 521 | bid = [] 522 | for e in data.values(): 523 | if e["type"] == 0: 524 | bid.append(e) 525 | else: 526 | ask.append(e) 527 | ask.sort(key=itemgetter("price")) 528 | bid.sort(key=itemgetter("price"), reverse=True) 529 | 530 | bid_str = ("{:.%df}" % digits).format(bid[0]["price"]) 531 | offer_str = ("{:.%df}" % digits).format(ask[0]["price"]) 532 | spread_str = ("{:.%df}" % digits).format(ask[0]["price"] - bid[0]["price"]) 533 | self.market_data_list[name] = { 534 | "bid": bid_str, 535 | "ask": offer_str, 536 | "spread": spread_str, 537 | "time": time.time(), 538 | } 539 | 540 | def close_all(self): 541 | self.fix.close_all() 542 | 543 | def cancel_all(self): 544 | self.fix.cancel_all() 545 | 546 | def logout(self): 547 | if self.isconnected(): 548 | self.fix.logout() 549 | logout = "Logged out" 550 | else: 551 | logout = "Not logged in" 552 | return logout 553 | 554 | def isconnected(self): 555 | return self.fix.logged 556 | -------------------------------------------------------------------------------- /ejtraderCT/api/fix.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | import threading 4 | from datetime import datetime 5 | import time 6 | from enum import IntEnum, Enum 7 | import socket 8 | from pprint import pformat 9 | from .buffer import Buffer 10 | 11 | 12 | class Field(IntEnum): 13 | AvgPx = 6 14 | BeginSeqNo = 7 15 | BeginString = 8 16 | BodyLength = 9 17 | CheckSum = 10 18 | ClOrdId = 11 19 | CumQty = 14 20 | OrdQty = 32 # nao tem na DOC 21 | MsgSeqNum = 34 22 | MsgType = 35 23 | OrderID = 37 24 | OrderQty = 38 25 | OrdStatus = 39 26 | OrdType = 40 27 | OrigClOrdID = 41 28 | Price = 44 29 | RefSeqNum = 45 30 | SenderCompID = 49 31 | SenderSubID = 50 32 | SendingTime = 52 33 | Side = 54 34 | Symbol = 55 35 | TargetCompID = 56 36 | TargetSubID = 57 37 | Text = 58 38 | TimeInForce = 59 39 | TransactTime = 60 40 | EncryptMethod = 98 41 | StopPx = 99 42 | OrdRejReason = 103 43 | HeartBtInt = 108 44 | TestReqID = 112 45 | ExpireTime = 126 46 | ResetSeqNumFlag = 141 47 | NoRelatedSym = 146 48 | ExecType = 150 49 | LeavesQty = 151 50 | MDReqID = 262 51 | SubscriptionRequestType = 263 52 | MarketDepth = 264 53 | MDUpdateType = 265 54 | NoMDEntryTypes = 267 55 | NoMDEntries = 268 56 | MDEntryType = 269 57 | MDEntryPx = 270 58 | MDEntrySize = 271 59 | MDEntryID = 278 60 | MDUpdateAction = 279 61 | SecurityReqID = 320 62 | SecurityResponseID = 322 63 | EncodedTextLen = 354 64 | EncodedText = 355 65 | RefTagID = 371 66 | RefMsgType = 372 67 | SessionRejectReason = 373 68 | BusinessRejectRefID = 379 69 | BusinessRejectReason = 380 70 | CxlRejResponseTo = 434 71 | Designation = 494 72 | Username = 553 73 | Password = 554 74 | SecurityListRequestType = 559 75 | SecurityRequestResult = 560 76 | MassStatusReqID = 584 77 | MassStatusReqType = 585 78 | NoPositions = 702 79 | LongQty = 704 80 | ShortQty = 705 81 | PosReqID = 710 82 | PosMaintRptID = 721 83 | TotalNumPosReports = 727 84 | PosReqResult = 728 85 | SettlPrice = 730 86 | TotNumReports = 911 87 | AbsoluteTP = 1000 88 | RelativeTP = 1001 89 | AbsoluteSL = 1002 90 | RelativeSL = 1003 91 | TrailingSL = 1004 92 | TriggerMethodSL = 1005 93 | GuaranteedSL = 1006 94 | SymbolName = 1007 95 | SymbolDigits = 1008 96 | 97 | 98 | class SubID(Enum): 99 | QUOTE = "QUOTE" 100 | TRADE = "TRADE" 101 | 102 | def __str__(self): 103 | return self.value 104 | 105 | 106 | class Side(IntEnum): 107 | Buy = 1 108 | Sell = 2 109 | 110 | 111 | class OrderType(IntEnum): 112 | Market = 1 113 | Limit = 2 114 | Stop = 3 115 | 116 | 117 | def get_time(): 118 | return datetime.utcnow().strftime("%Y%m%d-%H:%M:%S") 119 | 120 | 121 | class FIX: 122 | class Message: 123 | def __init__(self, sub: SubID = None, msg_type: str = None, parent=None): 124 | self.fields = [] 125 | if parent: 126 | self.origin = True 127 | self.fields.append((Field.BeginString, "FIX.4.4")) 128 | self.fields.append((Field.BodyLength, 0)) 129 | self.fields.append((Field.MsgType, msg_type)) 130 | self.fields.append( 131 | (Field.SenderCompID, parent.broker + "." + parent.login) 132 | ) 133 | self.fields.append((Field.SenderSubID, sub)) 134 | self.fields.append((Field.TargetCompID, "CSERVER")) 135 | self.fields.append((Field.TargetSubID, sub)) 136 | if sub == SubID.QUOTE: 137 | self.fields.append((Field.MsgSeqNum, parent.qseq)) 138 | parent.qseq += 1 139 | elif sub == SubID.TRADE: 140 | self.fields.append((Field.MsgSeqNum, parent.tseq)) 141 | parent.tseq += 1 142 | self.fields.append((Field.SendingTime, get_time())) 143 | else: 144 | self.origin = False 145 | 146 | def __getitem__(self, item): 147 | for k, v in self.fields: 148 | if k == item: 149 | return v 150 | return None 151 | 152 | def __setitem__(self, key, value): 153 | self.fields.append((key, value)) 154 | 155 | def get_repeating_groups(self, count_key, repeating_start, repeating_end=None): 156 | count = None 157 | result = [] 158 | item = {} 159 | for k, v in self.fields[8:]: 160 | if count == 0: 161 | return result 162 | if count is None: 163 | if k == count_key: 164 | count = int(v) 165 | continue 166 | if (k == repeating_start and len(item) > 0) or k == repeating_end: 167 | result.append(item) 168 | item = {} 169 | count -= 1 170 | item[k] = v 171 | result.append(item) 172 | return result 173 | 174 | def __bytes__(self): 175 | data = bytearray() 176 | for k, v in self.fields: 177 | data.extend(b"%b=%b\x01" % (str(k.value).encode(), str(v).encode())) 178 | if self.origin: 179 | data[12:13] = b"%d" % (len(data) - 14) 180 | cksm = sum(data) % 256 181 | data.extend(b"10=%03d\x01" % cksm) 182 | return bytes(data) 183 | 184 | def __str__(self): 185 | data = "" 186 | for k, v in self.fields: 187 | data += "%s=%s|" % (str(k.value), str(v)) 188 | if self.origin: 189 | data = data[:12] + "%d" % (len(data) - 14) + data[13:] 190 | cksm = sum(data.replace("|", "\x01").encode()) % 256 191 | data += "10=%03d|" % cksm 192 | return data 193 | 194 | def __repr__(self): 195 | return pformat([(k.name, v) for k, v in self.fields]) 196 | 197 | def __init__( 198 | self, 199 | server: str, 200 | broker: str, 201 | login: str, 202 | password: str, 203 | currency: str, 204 | client_id: str, 205 | position_list_callback, 206 | order_list_callback, 207 | update_fix_status=None, 208 | ): 209 | try: 210 | self.qstream = Buffer() 211 | self.qs = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 212 | self.qs.connect((server, 5201)) 213 | self.tstream = Buffer() 214 | self.ts = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 215 | self.ts.connect((server, 5202)) 216 | self.broker = broker 217 | self.login = login 218 | self.password = password 219 | self.currency = currency 220 | self.client_id = client_id 221 | self.qseq = 1 222 | self.tseq = 1 223 | self.qtest_seq = 1 224 | self.ttest_seq = 1 225 | self.market_seq = 1 226 | self.subscribed_symbol = [-1, -1, -1] 227 | self.qworker_thread = threading.Thread(target=self.qworker) 228 | self.qworker_thread.start() 229 | self.tworker_thread = threading.Thread(target=self.tworker) 230 | self.tworker_thread.start() 231 | self.ping_qworker_thread = None 232 | self.ping_tworker_thread = None 233 | self.sec_list_callback = None 234 | self.market_callback = None 235 | self.sec_id_table = {} 236 | self.sec_name_table = {} 237 | self.position_list_callback = position_list_callback 238 | self.order_list_callback = order_list_callback 239 | self.update_fix_status = update_fix_status 240 | self.market_data = {} 241 | self.position_list = {} 242 | self.spot_request_list = set() 243 | self.spot_price_list = {} 244 | self.base_convert_request_list = set() 245 | self.base_convert_list = {} 246 | self.order_list = {} 247 | self.origin_to_pos_id = {} 248 | self.origin_to_ord_id = {} 249 | self.logged = False 250 | self.logon() 251 | self.sec_list_evt = threading.Event() 252 | self.thread_sec_list = threading.Thread(target=self.sec_list) 253 | self.thread_sec_list.start() 254 | self.sec_list_evt.wait() 255 | except Exception as e: 256 | # Code to handle the exception 257 | logging.error(f"{e}") 258 | 259 | def qworker(self): 260 | while True: 261 | try: 262 | data = self.qs.recv(65535) 263 | except Exception as e: 264 | logging.info(e) 265 | break 266 | if len(data) == 0: 267 | logging.info("Quote Logged out") 268 | break 269 | try: 270 | self.qstream.write(data) 271 | self.parse_quote_message() 272 | except Exception as e: 273 | logging.info(f"Market is Close or Disconnected {e}") 274 | break 275 | 276 | def tworker(self): 277 | while True: 278 | try: 279 | data = self.ts.recv(65535) 280 | except Exception as e: 281 | logging.info(e) 282 | break 283 | if len(data) == 0: 284 | logging.info("Trade Logged out") 285 | break 286 | try: 287 | self.tstream.write(data) 288 | self.parse_trade_message() 289 | except Exception as e: 290 | logging.info(f"Market is Close or Logged out {e}") 291 | break 292 | 293 | def parse_quote_message(self): 294 | while len(self.qstream) > 0: 295 | match = re.search(rb"10=\d{3}\x01", self.qstream.peek(self.qstream.count())) 296 | if match: 297 | msg = FIX.Message() 298 | data = self.qstream.read(match.span()[1]).split(b"\x01")[:-1] 299 | for part in data: 300 | tag, value = part.split(b"=", 1) 301 | msg[Field(int(tag.decode()))] = value.decode() 302 | logging.debug("\033[32mRECV <<< %s\033[0m" % msg) 303 | self.process_message(msg) 304 | else: 305 | break 306 | 307 | def parse_trade_message(self): 308 | while len(self.tstream) > 0: 309 | match = re.search(rb"10=\d{3}\x01", self.tstream.peek(self.tstream.count())) 310 | if match: 311 | msg = FIX.Message() 312 | data = self.tstream.read(match.span()[1]).split(b"\x01")[:-1] 313 | for part in data: 314 | tag, value = part.split(b"=", 1) 315 | msg[Field(int(tag.decode()))] = value.decode() 316 | logging.debug("\033[92mRECV <<< %s\033[0m" % msg) 317 | self.process_message(msg) 318 | else: 319 | break 320 | 321 | def ping_qworker(self, interval: int): 322 | while True: 323 | if self.qs._closed: 324 | break 325 | self.qheartbeat() 326 | time.sleep(interval) 327 | 328 | def ping_tworker(self, interval: int): 329 | while True: 330 | if self.ts._closed: 331 | break 332 | self.theartbeat() 333 | time.sleep(interval) 334 | 335 | def process_ping(self, msg): 336 | pass 337 | 338 | def process_test(self, msg): 339 | if msg[Field.SenderSubID] == "QUOTE": 340 | self.qheartbeat(msg[Field.TestReqID]) 341 | elif msg[Field.SenderSubID] == "TRADE": 342 | self.theartbeat(msg[Field.TestReqID]) 343 | 344 | def process_logout(self, msg): 345 | if not msg[Field.Text]: 346 | self.logged = False 347 | self.update_fix_status(self.client_id, self.logged) 348 | 349 | def process_exec_report(self, msg): 350 | if msg[Field.ExecType] == "F": 351 | self.position_list = {} 352 | self.position_request() 353 | self.order_list = {} 354 | self.origin_to_pos_id[msg[Field.ClOrdId]] = msg[Field.PosMaintRptID] 355 | self.order_request() 356 | elif msg[Field.ExecType] in ["0", "4", "5", "C"]: 357 | self.order_list = {} 358 | self.order_request() 359 | elif msg[Field.ExecType] == "I": 360 | name = self.sec_id_table[int(msg[Field.Symbol])]["name"] 361 | self.order_list[msg[Field.OrderID]] = { 362 | "name": name, 363 | "side": Side(int(msg[Field.Side])), 364 | "amount": float(msg[Field.LeavesQty]), 365 | "type": int(msg[Field.OrdType]), 366 | "pos_id": msg[Field.PosMaintRptID], 367 | "digits": self.sec_id_table[int(msg[Field.Symbol])]["digits"], 368 | "clid": msg[Field.ClOrdId], 369 | } 370 | 371 | if int(msg[Field.OrdType]) == 1: 372 | self.origin_to_pos_id[msg[Field.ClOrdId]] = msg[Field.PosMaintRptID] 373 | else: 374 | if msg[Field.ClOrdId] not in self.origin_to_ord_id: 375 | self.origin_to_ord_id[msg[Field.ClOrdId]] = [] 376 | if msg[Field.OrderID] not in self.origin_to_ord_id[msg[Field.ClOrdId]]: 377 | self.origin_to_ord_id[msg[Field.ClOrdId]].append(msg[Field.OrderID]) 378 | 379 | if int(msg[Field.OrdType]) > 1: 380 | price = msg[Field.Price] 381 | if price: 382 | self.order_list[msg[Field.OrderID]]["price"] = float(price) 383 | else: 384 | self.order_list[msg[Field.OrderID]]["price"] = float( 385 | msg[Field.StopPx] 386 | ) 387 | if name not in self.spot_request_list: 388 | self.spot_market_request(name) 389 | self.order_list_callback( 390 | self.order_list, self.spot_price_list, self.client_id 391 | ) 392 | 393 | def process_logon(self, msg): 394 | if msg[Field.SenderSubID] == "QUOTE": 395 | logging.info("Quote logged on") 396 | self.ping_qworker_thread = threading.Thread( 397 | target=self.ping_qworker, args=[int(msg[Field.HeartBtInt])] 398 | ) 399 | self.ping_qworker_thread.start() 400 | self.logged = True 401 | elif msg[Field.SenderSubID] == "TRADE": 402 | logging.info("Trade logged on") 403 | self.ping_tworker_thread = threading.Thread( 404 | target=self.ping_tworker, args=[int(msg[Field.HeartBtInt])] 405 | ) 406 | self.ping_tworker_thread.start() 407 | 408 | def process_market_data(self, msg: Message): 409 | name = self.sec_id_table[int(msg[Field.Symbol])]["name"] 410 | digits = self.sec_id_table[int(msg[Field.Symbol])]["digits"] 411 | entries = msg.get_repeating_groups(Field.NoMDEntries, Field.MDEntryType) 412 | if not msg[Field.MDEntryID] and msg[Field.NoMDEntries] != "0": 413 | self.spot_price_list[name] = {} 414 | for e in entries: 415 | self.spot_price_list[name]["time"] = int(round(time.time() * 1000)) 416 | self.spot_price_list[name][ 417 | "bid" if e[Field.MDEntryType] == "0" else "ask" 418 | ] = float(e[Field.MDEntryPx]) 419 | self.position_list_callback( 420 | self.position_list, self.spot_price_list, self.client_id 421 | ) 422 | self.order_list_callback( 423 | self.order_list, self.spot_price_list, self.client_id 424 | ) 425 | return 426 | self.market_data[name] = {} 427 | for e in entries: 428 | eid = e[Field.MDEntryID] 429 | self.market_data[name][eid] = { 430 | "type": int(e[Field.MDEntryType]), 431 | "price": float(e[Field.MDEntryPx]), 432 | "size": float(e[Field.MDEntrySize]), 433 | } 434 | # logging.debug(pformat(msg)) 435 | self.market_callback(name, digits, self.market_data[name]) 436 | 437 | def process_market_incr_data(self, msg: Message): 438 | name = self.sec_id_table[int(msg[Field.Symbol])]["name"] 439 | digits = self.sec_id_table[int(msg[Field.Symbol])]["digits"] 440 | entries = msg.get_repeating_groups(Field.NoMDEntries, Field.MDUpdateAction) 441 | for e in entries: 442 | if e[Field.MDUpdateAction] == "2": 443 | del self.market_data[name][e[Field.MDEntryID]] 444 | elif e[Field.MDUpdateAction] == "0": 445 | eid = e[Field.MDEntryID] 446 | self.market_data[name][eid] = { 447 | "type": int(e[Field.MDEntryType]), 448 | "price": float(e[Field.MDEntryPx]), 449 | "size": float(e[Field.MDEntrySize]), 450 | } 451 | # logging.debug(pformat(msg)) 452 | self.market_callback(name, digits, self.market_data[name]) 453 | 454 | def process_sec_list(self, msg): 455 | sec_list = msg.get_repeating_groups(Field.NoRelatedSym, Field.Symbol) 456 | for symbol in sec_list: 457 | self.sec_id_table[int(symbol[Field.Symbol])] = { 458 | "name": symbol[Field.SymbolName], 459 | "digits": int(symbol[Field.SymbolDigits]), 460 | } 461 | self.sec_name_table[symbol[Field.SymbolName]] = { 462 | "id": int(symbol[Field.Symbol]), 463 | "digits": int(symbol[Field.SymbolDigits]), 464 | } 465 | if self.sec_list_callback is not None: 466 | self.sec_list_callback() 467 | self.position_request() 468 | self.order_request() 469 | self.sec_list_evt.set() 470 | 471 | def get_origin_from_pos_id(self, pos_id): 472 | keys = list(self.origin_to_pos_id.keys()) 473 | values = list(self.origin_to_pos_id.values()) 474 | if pos_id in values: 475 | return keys[values.index(pos_id)] 476 | else: 477 | return None 478 | 479 | def process_position_list(self, msg): 480 | if msg[Field.PosReqResult] == "2": 481 | return 482 | name = self.sec_id_table[int(msg[Field.Symbol])]["name"] 483 | self.position_list[msg[Field.PosMaintRptID]] = { 484 | "pos_id": msg[Field.PosMaintRptID], 485 | "name": name, 486 | "long": float(msg[Field.LongQty]), 487 | "short": float(msg[Field.ShortQty]), 488 | "price": float(msg[Field.SettlPrice]), 489 | "digits": self.sec_id_table[int(msg[Field.Symbol])]["digits"], 490 | "clid": self.get_origin_from_pos_id(msg[Field.PosMaintRptID]), 491 | } 492 | 493 | if name not in self.spot_request_list: 494 | self.spot_market_request(name) 495 | base = name[-3:] 496 | if base != self.currency: 497 | pair = "%s%s" % (base, self.currency) 498 | conv_dir = 0 499 | if not self.sec_name_table.get(pair, None): 500 | pair = "%s%s" % (self.currency, base) 501 | conv_dir = 1 502 | self.position_list[msg[Field.PosMaintRptID]]["convert"] = pair 503 | self.position_list[msg[Field.PosMaintRptID]]["convert_dir"] = conv_dir 504 | if pair not in self.spot_request_list: 505 | self.spot_market_request(pair) 506 | self.position_list_callback( 507 | self.position_list, self.spot_price_list, self.client_id 508 | ) 509 | 510 | def process_reject(self, msg): 511 | checkOrders = msg[Field.Text].split(":")[1] 512 | if checkOrders == "no orders found": 513 | logging.info("No Orders") 514 | else: 515 | logging.error(checkOrders) 516 | 517 | message_dispatch = { 518 | "0": process_ping, 519 | "1": process_test, 520 | "3": process_reject, 521 | "5": process_logout, 522 | "8": process_exec_report, 523 | "9": process_reject, 524 | "A": process_logon, 525 | "j": process_reject, 526 | "W": process_market_data, 527 | "X": process_market_incr_data, 528 | "y": process_sec_list, 529 | "AP": process_position_list, 530 | } 531 | 532 | def process_message(self, msg: Message): 533 | msg_type = msg[Field.MsgType] 534 | FIX.message_dispatch[msg_type](self, msg) 535 | 536 | def send_message(self, msg: Message): 537 | if msg[Field.TargetSubID] == SubID.QUOTE: 538 | try: 539 | self.qs.send(bytes(msg)) 540 | logging.debug("\033[36mSEND >>> %s\033[0m" % msg) 541 | except Exception as e: 542 | logging.debug(f"QUOTE send error: {e}. client_id: {self.client_id}") 543 | self.qs.close() 544 | elif msg[Field.TargetSubID] == SubID.TRADE: 545 | try: 546 | self.ts.send(bytes(msg)) 547 | logging.debug("\033[96mSEND >>> %s\033[0m" % msg) 548 | except Exception as e: 549 | logging.debug( 550 | f"TRADE send error: {e} Closing connection. client_id:{self.client_id}" 551 | ) 552 | self.ts.close() 553 | 554 | def qheartbeat(self, test_id: int = None): 555 | msg = FIX.Message(SubID.QUOTE, "0", self) 556 | if test_id: 557 | msg[Field.TestReqID] = test_id 558 | self.send_message(msg) 559 | 560 | def theartbeat(self, test_id: int = None): 561 | msg = FIX.Message(SubID.TRADE, "0", self) 562 | if test_id: 563 | msg[Field.TestReqID] = test_id 564 | self.send_message(msg) 565 | 566 | def test(self): 567 | msg = FIX.Message(SubID.QUOTE, "1", self) 568 | msg[Field.TestReqID] = self.qtest_seq 569 | self.qtest_seq += 1 570 | self.send_message(msg) 571 | 572 | msg = FIX.Message(SubID.TRADE, "1", self) 573 | msg[Field.TestReqID] = self.ttest_seq 574 | self.ttest_seq += 1 575 | self.send_message(msg) 576 | 577 | def logon(self): 578 | msg = FIX.Message(SubID.QUOTE, "A", self) 579 | msg[Field.EncryptMethod] = 0 580 | msg[Field.HeartBtInt] = 30 581 | msg[Field.Username] = self.login 582 | msg[Field.Password] = self.password 583 | self.send_message(msg) 584 | msg = FIX.Message(SubID.TRADE, "A", self) 585 | msg[Field.EncryptMethod] = 0 586 | msg[Field.HeartBtInt] = 30 587 | msg[Field.Username] = self.login 588 | msg[Field.Password] = self.password 589 | self.send_message(msg) 590 | 591 | def logout(self): 592 | msg = FIX.Message(SubID.QUOTE, "5", self) 593 | self.send_message(msg) 594 | msg = FIX.Message(SubID.TRADE, "5", self) 595 | self.send_message(msg) 596 | 597 | def market_request(self, subid, symbol, callback): 598 | if symbol not in self.sec_name_table.keys(): 599 | logging.error("Symbol %s not found!" % symbol) 600 | return 601 | 602 | if self.subscribed_symbol[subid] != -1: 603 | msg = FIX.Message(SubID.QUOTE, "V", self) 604 | msg[Field.MDReqID] = self.market_seq 605 | msg[Field.SubscriptionRequestType] = 2 606 | msg[Field.MarketDepth] = 0 607 | msg[Field.NoMDEntryTypes] = 2 608 | msg[Field.MDEntryType] = 0 609 | msg[Field.MDEntryType] = 1 610 | msg[Field.NoRelatedSym] = 1 611 | msg[Field.Symbol] = self.subscribed_symbol[subid] 612 | self.send_message(msg) 613 | self.market_seq += 1 614 | msg = FIX.Message(SubID.QUOTE, "V", self) 615 | msg[Field.MDReqID] = self.market_seq 616 | msg[Field.SubscriptionRequestType] = 1 617 | msg[Field.MarketDepth] = 0 618 | msg[Field.NoMDEntryTypes] = 2 619 | msg[Field.MDEntryType] = 0 620 | msg[Field.MDEntryType] = 1 621 | msg[Field.NoRelatedSym] = 1 622 | msg[Field.Symbol] = self.sec_name_table[symbol]["id"] 623 | self.subscribed_symbol[subid] = msg[Field.Symbol] 624 | self.market_callback = callback 625 | self.send_message(msg) 626 | self.market_seq += 1 627 | 628 | def spot_market_request(self, symbol): 629 | msg = FIX.Message(SubID.QUOTE, "V", self) 630 | msg[Field.MDReqID] = self.client_id 631 | msg[Field.SubscriptionRequestType] = 1 632 | msg[Field.MarketDepth] = 1 633 | msg[Field.NoMDEntryTypes] = 2 634 | msg[Field.MDEntryType] = 0 635 | msg[Field.MDEntryType] = 1 636 | msg[Field.NoRelatedSym] = 1 637 | msg[Field.Symbol] = self.sec_name_table[symbol]["id"] 638 | self.spot_request_list.add(symbol) 639 | self.send_message(msg) 640 | 641 | def position_request(self): 642 | msg = FIX.Message(SubID.TRADE, "AN", self) 643 | msg[Field.PosReqID] = self.client_id 644 | self.send_message(msg) 645 | 646 | def order_request(self): 647 | msg = FIX.Message(SubID.TRADE, "AF", self) 648 | msg[Field.MassStatusReqID] = self.client_id 649 | msg[Field.MassStatusReqType] = 7 650 | self.send_message(msg) 651 | 652 | def sec_list(self, callback=None): 653 | msg = FIX.Message(SubID.QUOTE, "x", self) 654 | msg[Field.SecurityReqID] = self.client_id 655 | msg[Field.SecurityListRequestType] = 0 656 | self.sec_list_callback = callback 657 | self.send_message(msg) 658 | 659 | def new_market_order( 660 | self, symbol, side: Side, size: float, originId=None, pos_id=None 661 | ): 662 | logging.info(f"error ORIGINAL ID: {originId}") 663 | logging.info(f"error POS ID: {pos_id}") 664 | if symbol not in self.sec_name_table: 665 | return 666 | 667 | msg = FIX.Message(SubID.TRADE, "D", self) 668 | msg[Field.ClOrdId] = originId if originId else "dt" + get_time() 669 | msg[Field.Symbol] = self.sec_name_table[symbol]["id"] 670 | msg[Field.Side] = side.value 671 | msg[Field.TransactTime] = get_time() 672 | msg[Field.OrderQty] = size 673 | msg[Field.OrdType] = OrderType.Market.value 674 | msg[Field.Designation] = f"EjtraderCT ClientID: {self.client_id}" 675 | if pos_id: 676 | msg[Field.PosMaintRptID] = pos_id 677 | 678 | self.send_message(msg) 679 | 680 | def close_position(self, pos_id: str, lots): 681 | if pos_id not in self.position_list: 682 | return 683 | 684 | # remove referencia ao server ord_id da tabela de-para 685 | for o, p in self.origin_to_pos_id.items(): 686 | if p == pos_id: 687 | # cancela ordens de TP e SL se existirem 688 | if o in self.origin_to_ord_id: 689 | for cl_id in self.origin_to_ord_id[o]: 690 | self.cancel_order(cl_id) 691 | self.origin_to_pos_id.pop(o) 692 | break 693 | 694 | msg = FIX.Message(SubID.TRADE, "D", self) 695 | msg[Field.ClOrdId] = get_time() 696 | msg[Field.Symbol] = self.sec_name_table[self.position_list[pos_id]["name"]][ 697 | "id" 698 | ] 699 | msg[Field.Side] = ( 700 | Side.Sell.value 701 | if self.position_list[pos_id]["long"] > 0 702 | else Side.Buy.value 703 | ) 704 | msg[Field.TransactTime] = get_time() 705 | msg[Field.OrderQty] = ( 706 | lots 707 | if lots is not None 708 | else self.position_list[pos_id]["long"] 709 | if msg[Field.Side] == Side.Sell 710 | else self.position_list[pos_id]["short"] 711 | ) 712 | msg[Field.PosMaintRptID] = pos_id 713 | msg[Field.OrdType] = OrderType.Market.value 714 | self.send_message(msg) 715 | 716 | def close_all(self): 717 | for position in self.position_list: 718 | self.close_position(position, None) 719 | 720 | def new_limit_order( 721 | self, 722 | symbol, 723 | side: Side, 724 | order_type: OrderType, 725 | size: float, 726 | price: float, 727 | originId=None, 728 | pos_id=None, 729 | ): 730 | logging.info(f"error ORIGINAL ID: {originId}") 731 | logging.info(f"error POS ID: {pos_id}") 732 | if symbol not in self.sec_name_table: 733 | return 734 | 735 | msg = FIX.Message(SubID.TRADE, "D", self) 736 | msg[Field.ClOrdId] = originId if originId else "dt" + get_time() 737 | msg[Field.Symbol] = self.sec_name_table[symbol]["id"] 738 | msg[Field.Side] = side.value 739 | msg[Field.TransactTime] = get_time() 740 | msg[Field.OrderQty] = size 741 | msg[Field.OrdType] = order_type.value 742 | if order_type == OrderType.Limit: 743 | msg[Field.Price] = price 744 | elif order_type == OrderType.Stop: 745 | msg[Field.StopPx] = price 746 | if pos_id: 747 | msg[Field.PosMaintRptID] = pos_id 748 | self.send_message(msg) 749 | 750 | def cancel_order(self, clid: str): 751 | # remove referencia ao server ord_id da tabela de-para 752 | for o, p in self.origin_to_ord_id.items(): 753 | if clid in p: 754 | p.remove(clid) 755 | if len(p) == 0: 756 | self.origin_to_ord_id.pop(o) 757 | break 758 | 759 | msg = FIX.Message(SubID.TRADE, "F", self) 760 | msg[Field.OrigClOrdID] = clid 761 | msg[Field.OrderID] = clid 762 | msg[Field.ClOrdId] = clid 763 | self.send_message(msg) 764 | 765 | def cancel_all(self): 766 | for order in self.order_list: 767 | self.cancel_order(order) 768 | -------------------------------------------------------------------------------- /ejtraderCT/api/math.py: -------------------------------------------------------------------------------- 1 | __all__ = ['calculate_commission', 'calculate_pip_value', 'calculate_spread'] 2 | 3 | 4 | def calculate_spread(bid: str, ask: str, pip_position: int) -> int: 5 | spread = float(ask) - float(bid) 6 | spread = '{:.{}f}'.format(spread, pip_position + 1) 7 | return int(spread.replace('.', '')) 8 | 9 | 10 | def calculate_pip_value(price: str, size: int, pip_position: int) -> str: 11 | pip = (pow(1 / 10, pip_position) * size) / float(price) 12 | pip = '{:.5f}'.format(pip) 13 | return pip 14 | 15 | 16 | def calculate_commission(size=10000, rate=1, commission=0.000030): 17 | # can't handle different size/rate for now 18 | return (size * commission) * rate * 2 -------------------------------------------------------------------------------- /examples/restapi/.env_example: -------------------------------------------------------------------------------- 1 | HOST_NAME=your_host_name 2 | SENDER_COMPID=your_sender_compid 3 | 4 | PASSWORD=your_password 5 | -------------------------------------------------------------------------------- /examples/restapi/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Python runtime based on Alpine as a parent image 2 | FROM python:3.9-alpine 3 | 4 | # Set the working directory in the container to /app 5 | WORKDIR /app 6 | 7 | # Add the current directory contents into the container at /app 8 | ADD . /app 9 | 10 | # Set environment variables 11 | ENV PYTHONDONTWRITEBYTECODE 1 12 | ENV PYTHONUNBUFFERED 1 13 | ENV HOST_NAME="server" 14 | ENV SENDER_COMPID="accout" 15 | ENV PASSWORD="password" 16 | 17 | # Install any needed packages specified in requirements.txt 18 | RUN apk add --update --no-cache --virtual .tmp gcc libc-dev linux-headers 19 | RUN pip install --no-cache-dir -r requirements.txt 20 | RUN apk del .tmp 21 | 22 | # Expose port 8000 in the container 23 | EXPOSE 8000 24 | 25 | # Run the application 26 | CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"] 27 | -------------------------------------------------------------------------------- /examples/restapi/EjtraderCT RestAPI.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "1758d6fa-7479-47f8-beb0-bd0018c63e12", 4 | "name": "EjtraderCT RestAPI", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", 6 | "_exporter_id": "20696130" 7 | }, 8 | "item": [ 9 | { 10 | "name": "Connection Status", 11 | "request": { 12 | "method": "POST", 13 | "header": [], 14 | "body": { 15 | "mode": "raw", 16 | "raw": "", 17 | "options": { 18 | "raw": { 19 | "language": "json" 20 | } 21 | } 22 | }, 23 | "url": { 24 | "raw": "{{base_url}}/status", 25 | "host": [ 26 | "{{base_url}}" 27 | ], 28 | "path": [ 29 | "status" 30 | ] 31 | } 32 | }, 33 | "response": [] 34 | }, 35 | { 36 | "name": "reconnect", 37 | "request": { 38 | "method": "POST", 39 | "header": [], 40 | "body": { 41 | "mode": "raw", 42 | "raw": "{\n \n}", 43 | "options": { 44 | "raw": { 45 | "language": "json" 46 | } 47 | } 48 | }, 49 | "url": { 50 | "raw": "{{base_url}}/login", 51 | "host": [ 52 | "{{base_url}}" 53 | ], 54 | "path": [ 55 | "login" 56 | ] 57 | } 58 | }, 59 | "response": [] 60 | }, 61 | { 62 | "name": "Logout", 63 | "request": { 64 | "method": "POST", 65 | "header": [], 66 | "body": { 67 | "mode": "raw", 68 | "raw": "", 69 | "options": { 70 | "raw": { 71 | "language": "json" 72 | } 73 | } 74 | }, 75 | "url": { 76 | "raw": "{{base_url}}/logout", 77 | "host": [ 78 | "{{base_url}}" 79 | ], 80 | "path": [ 81 | "logout" 82 | ] 83 | } 84 | }, 85 | "response": [] 86 | }, 87 | { 88 | "name": "Subscribe to symbols", 89 | "request": { 90 | "method": "POST", 91 | "header": [], 92 | "body": { 93 | "mode": "raw", 94 | "raw": "{\n \"symbols\": [\"EURUSD\", \"GBPUSD\"]\n}", 95 | "options": { 96 | "raw": { 97 | "language": "json" 98 | } 99 | } 100 | }, 101 | "url": { 102 | "raw": "{{base_url}}/subscribe", 103 | "host": [ 104 | "{{base_url}}" 105 | ], 106 | "path": [ 107 | "subscribe" 108 | ] 109 | } 110 | }, 111 | "response": [] 112 | }, 113 | { 114 | "name": "Get quotes", 115 | "request": { 116 | "method": "GET", 117 | "header": [], 118 | "url": { 119 | "raw": "{{base_url}}/quote", 120 | "host": [ 121 | "{{base_url}}" 122 | ], 123 | "path": [ 124 | "quote" 125 | ] 126 | } 127 | }, 128 | "response": [] 129 | }, 130 | { 131 | "name": "Get single quote", 132 | "request": { 133 | "method": "GET", 134 | "header": [], 135 | "url": { 136 | "raw": "{{base_url}}/quote/EURUSD", 137 | "host": [ 138 | "{{base_url}}" 139 | ], 140 | "path": [ 141 | "quote", 142 | "EURUSD" 143 | ] 144 | } 145 | }, 146 | "response": [] 147 | }, 148 | { 149 | "name": "Buy", 150 | "request": { 151 | "method": "POST", 152 | "header": [], 153 | "body": { 154 | "mode": "raw", 155 | "raw": "{\n \"symbol\": \"EURUSD\",\n \"volume\": 0.01,\n \"stoploss\": 0,\n \"takeprofit\": 0\n}", 156 | "options": { 157 | "raw": { 158 | "language": "json" 159 | } 160 | } 161 | }, 162 | "url": { 163 | "raw": "{{base_url}}/buy", 164 | "host": [ 165 | "{{base_url}}" 166 | ], 167 | "path": [ 168 | "buy" 169 | ] 170 | } 171 | }, 172 | "response": [] 173 | }, 174 | { 175 | "name": "Sell", 176 | "request": { 177 | "method": "POST", 178 | "header": [], 179 | "body": { 180 | "mode": "raw", 181 | "raw": "{\n \"symbol\": \"EURUSD\",\n \"volume\": 0.01,\n \"stoploss\": 1.19,\n \"takeprofit\": 1.18\n}", 182 | "options": { 183 | "raw": { 184 | "language": "json" 185 | } 186 | } 187 | }, 188 | "url": { 189 | "raw": "{{base_url}}/sell", 190 | "host": [ 191 | "{{base_url}}" 192 | ], 193 | "path": [ 194 | "sell" 195 | ] 196 | } 197 | }, 198 | "response": [] 199 | }, 200 | { 201 | "name": "Buy Limit", 202 | "request": { 203 | "method": "POST", 204 | "header": [], 205 | "body": { 206 | "mode": "raw", 207 | "raw": "{\n \"symbol\": \"EURUSD\",\n \"volume\": 0.01,\n \"price\": 1.18\n}", 208 | "options": { 209 | "raw": { 210 | "language": "json" 211 | } 212 | } 213 | }, 214 | "url": { 215 | "raw": "{{base_url}}/buyLimit", 216 | "host": [ 217 | "{{base_url}}" 218 | ], 219 | "path": [ 220 | "buyLimit" 221 | ] 222 | } 223 | }, 224 | "response": [] 225 | }, 226 | { 227 | "name": "Sell Limit", 228 | "request": { 229 | "method": "POST", 230 | "header": [], 231 | "body": { 232 | "mode": "raw", 233 | "raw": "{\n \"symbol\": \"EURUSD\",\n \"volume\": 0.01,\n \"price\": 1.18\n}", 234 | "options": { 235 | "raw": { 236 | "language": "json" 237 | } 238 | } 239 | }, 240 | "url": { 241 | "raw": "{{base_url}}/sellLimit", 242 | "host": [ 243 | "{{base_url}}" 244 | ], 245 | "path": [ 246 | "sellLimit" 247 | ] 248 | } 249 | }, 250 | "response": [] 251 | }, 252 | { 253 | "name": "Buy Stop", 254 | "request": { 255 | "method": "POST", 256 | "header": [], 257 | "body": { 258 | "mode": "raw", 259 | "raw": "{\n \"symbol\": \"EURUSD\",\n \"volume\": 0.01,\n \"price\": 1.18\n}", 260 | "options": { 261 | "raw": { 262 | "language": "json" 263 | } 264 | } 265 | }, 266 | "url": { 267 | "raw": "{{base_url}}/buyStop", 268 | "host": [ 269 | "{{base_url}}" 270 | ], 271 | "path": [ 272 | "buyStop" 273 | ] 274 | } 275 | }, 276 | "response": [] 277 | }, 278 | { 279 | "name": "Sell Stop", 280 | "request": { 281 | "method": "POST", 282 | "header": [], 283 | "body": { 284 | "mode": "raw", 285 | "raw": "{\n \"symbol\": \"EURUSD\",\n \"volume\": 0.01,\n \"price\": 1.18\n}", 286 | "options": { 287 | "raw": { 288 | "language": "json" 289 | } 290 | } 291 | }, 292 | "url": { 293 | "raw": "{{base_url}}/sellStop", 294 | "host": [ 295 | "{{base_url}}" 296 | ], 297 | "path": [ 298 | "sellStop" 299 | ] 300 | } 301 | }, 302 | "response": [] 303 | }, 304 | { 305 | "name": "List Positions", 306 | "request": { 307 | "method": "GET", 308 | "header": [], 309 | "url": { 310 | "raw": "{{base_url}}/positions", 311 | "host": [ 312 | "{{base_url}}" 313 | ], 314 | "path": [ 315 | "positions" 316 | ] 317 | } 318 | }, 319 | "response": [] 320 | }, 321 | { 322 | "name": "List Orders", 323 | "request": { 324 | "method": "GET", 325 | "header": [], 326 | "url": { 327 | "raw": "{{base_url}}/orders", 328 | "host": [ 329 | "{{base_url}}" 330 | ], 331 | "path": [ 332 | "orders" 333 | ] 334 | } 335 | }, 336 | "response": [] 337 | }, 338 | { 339 | "name": "Cancel Order", 340 | "request": { 341 | "method": "POST", 342 | "header": [], 343 | "body": { 344 | "mode": "raw", 345 | "raw": "{\n \"id\": \"order_id\"\n}", 346 | "options": { 347 | "raw": { 348 | "language": "json" 349 | } 350 | } 351 | }, 352 | "url": { 353 | "raw": "{{base_url}}/orderCancelById", 354 | "host": [ 355 | "{{base_url}}" 356 | ], 357 | "path": [ 358 | "orderCancelById" 359 | ] 360 | } 361 | }, 362 | "response": [] 363 | }, 364 | { 365 | "name": "Close Position", 366 | "request": { 367 | "method": "POST", 368 | "header": [], 369 | "body": { 370 | "mode": "raw", 371 | "raw": "{\n \"id\": \"position_id\",\n \"amount\": \"amount\"\n}", 372 | "options": { 373 | "raw": { 374 | "language": "json" 375 | } 376 | } 377 | }, 378 | "url": { 379 | "raw": "{{base_url}}/positionCloseById", 380 | "host": [ 381 | "{{base_url}}" 382 | ], 383 | "path": [ 384 | "positionCloseById" 385 | ] 386 | } 387 | }, 388 | "response": [] 389 | }, 390 | { 391 | "name": "Cancel All Orders", 392 | "request": { 393 | "method": "POST", 394 | "header": [], 395 | "url": { 396 | "raw": "{{base_url}}/cancel_all", 397 | "host": [ 398 | "{{base_url}}" 399 | ], 400 | "path": [ 401 | "cancel_all" 402 | ] 403 | } 404 | }, 405 | "response": [] 406 | }, 407 | { 408 | "name": "Close All Positions", 409 | "request": { 410 | "method": "POST", 411 | "header": [], 412 | "url": { 413 | "raw": "{{base_url}}/close_all", 414 | "host": [ 415 | "{{base_url}}" 416 | ], 417 | "path": [ 418 | "close_all" 419 | ] 420 | } 421 | }, 422 | "response": [] 423 | } 424 | ] 425 | } -------------------------------------------------------------------------------- /examples/restapi/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## How to Run Docker Compose 3 | 4 | This repository uses Docker Compose to manage the execution of the "ejtrader/ejtraderct:rest" application. Follow the instructions below to run the project on your local machine. 5 | 6 | ### Prerequisites 7 | 8 | Before getting started, make sure you have Docker and Docker Compose installed on your system. You can find information on how to install them at [https://docs.docker.com/get-docker/](https://docs.docker.com/get-docker/) and [https://docs.docker.com/compose/install/](https://docs.docker.com/compose/install/). 9 | 10 | ### Steps to Run 11 | 12 | 1. Clone this repository to your local environment. 13 | 14 | 2. In the root directory of the project, create a file named `.env` and configure the required environment variables for the application. Make sure to fill in all the required variables. 15 | 16 | 3. Open a terminal and navigate to the root directory of the project. 17 | 18 | 4. Run the following command to start the application using Docker Compose: 19 | 20 | This command will build the necessary containers and start the application. The output will be displayed in the terminal. 21 | 22 | Once the application is up and running, you can access it in using postman at http://localhost:8000. 23 | That's it! You have successfully run the "ejtrader/ejtraderct:rest" application using Docker Compose. 24 | 25 | Note: If you want to run the containers in the background (detached mode), you can use the -d flag: 26 | 27 | ```shell 28 | docker-compose up -d 29 | ``` 30 | 31 | 32 | Please add your credentials to the `env_example` file and rename it to `.env`. Then, run the following command: 33 | 34 | ```shell 35 | docker-compose up 36 | ``` 37 | 38 | or 39 | 40 | 41 | ```shell 42 | docker build --build-arg HOST_NAME=your_host_name --build-arg SENDER_COMPID=your_sender_compid --build-arg PASSWORD=your_password -t ejtrader/ejtraderct:rest . 43 | ``` 44 | 45 | ```shell 46 | docker run -e "HOST_NAME=your_host_name" -e "SENDER_COMPID=your_sender_compid" -e "PASSWORD=your_password" ejtrader/ejtraderct:rest 47 | ``` 48 | 49 | 50 | ### Option 2: Use Pre-Built Image with Docker Compose 51 | Prerequisites 52 | 53 | Before getting started, make sure you have Docker Compose installed on your system. You can find information on how to install Docker Compose at https://docs.docker.com/compose/install/. 54 | 55 | Steps to Run 56 | 57 | Copy the contents of the docker-compose.yml file below and paste it into a file named docker-compose.yml in the root directory of your project: 58 | ```yml 59 | version: '3.8' 60 | services: 61 | ejtraderctrest: 62 | image: ejtrader/ejtraderct:rest 63 | container_name: rabbitmq 64 | restart: always 65 | environment: 66 | HOST_NAME: "68.205.95.21" 67 | SENDER_COMPID: "live.icmarkets.1104926" 68 | PASSWORD: "12345678" 69 | 70 | ports: 71 | - "8000:8000" 72 | env_file: 73 | - .env 74 | ``` 75 | 76 | In the root directory of the project, open a terminal. 77 | Run the following command to start the application using Docker Compose: 78 | For Windows and Linux: 79 | 80 | ```shell 81 | docker-compose up -d 82 | ``` 83 | 84 | For macOS: 85 | 86 | ```shell 87 | docker compose up -d 88 | ``` 89 | -------------------------------------------------------------------------------- /examples/restapi/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pydantic import BaseModel 3 | from fastapi import FastAPI 4 | from ejtraderCT import Ctrader 5 | 6 | 7 | app = FastAPI() 8 | global api 9 | api = Ctrader(os.getenv("HOST_NAME"), os.getenv("SENDER_COMPID"), os.getenv("PASSWORD")) 10 | 11 | 12 | class LoginModel(BaseModel): 13 | server: str 14 | account: str 15 | password: str 16 | 17 | 18 | class SymbolModel(BaseModel): 19 | symbols: list[str] 20 | 21 | 22 | class OrderModel(BaseModel): 23 | symbol: str 24 | volume: float 25 | stoploss: float 26 | takeprofit: float 27 | price: float = None 28 | 29 | 30 | class ModifyModel(BaseModel): 31 | id: str 32 | stoploss: float 33 | takeprofit: float 34 | price: float = None 35 | 36 | 37 | class IdModel(BaseModel): 38 | id: str 39 | 40 | 41 | async def check(): 42 | return api.isconnected() 43 | 44 | 45 | @app.post("/login") 46 | async def login(): 47 | api = Ctrader( 48 | os.getenv("HOST_NAME"), os.getenv("SENDER_COMPID"), os.getenv("PASSWORD") 49 | ) 50 | return {"connected": api.isconnected()} 51 | 52 | 53 | @app.get("/quote/{symbol}") 54 | async def quote(symbol: str = None): 55 | if symbol: 56 | quote = api.quote(symbol) 57 | else: 58 | quote = api.quote() 59 | return quote 60 | 61 | 62 | @app.post("/buy") 63 | async def buy(order: OrderModel): 64 | return { 65 | "Position": api.buy( 66 | order.symbol, order.volume, order.stoploss, order.takeprofit 67 | ) 68 | } 69 | 70 | 71 | @app.post("/sell") 72 | async def sell(order: OrderModel): 73 | return { 74 | "Position": api.sell( 75 | order.symbol, order.volume, order.stoploss, order.takeprofit 76 | ) 77 | } 78 | 79 | 80 | @app.post("/buyLimit") 81 | async def buy_limit(order: OrderModel): 82 | return {"Order": api.buyLimit(order.symbol, order.volume, order.price)} 83 | 84 | 85 | @app.post("/sellLimit") 86 | async def sell_limit(order: OrderModel): 87 | return {"Order": api.sellLimit(order.symbol, order.volume, order.price)} 88 | 89 | 90 | @app.post("/buyStop") 91 | async def buy_stop(order: OrderModel): 92 | return {"Order": api.buyStop(order.symbol, order.volume, order.price)} 93 | 94 | 95 | @app.post("/sellStop") 96 | async def sell_stop(order: OrderModel): 97 | return {"Order": api.sellStop(order.symbol, order.volume, order.price)} 98 | 99 | 100 | @app.get("/positions") 101 | async def positions(): 102 | return api.positions() 103 | 104 | 105 | @app.get("/orders") 106 | async def orders(): 107 | return api.orders() 108 | 109 | 110 | @app.post("/orderCancelById") 111 | async def order_cancel_by_id(id_model: IdModel): 112 | api.orderCancelById(id_model.id) 113 | return {"message": "Order cancelled"} 114 | 115 | 116 | @app.post("/positionCloseById") 117 | async def position_close_by_id(id_model: IdModel): 118 | api.positionCloseById(id_model.id) 119 | return {"message": "Position closed"} 120 | 121 | 122 | @app.post("/cancel_all") 123 | async def cancel_all(): 124 | api.cancel_all() 125 | return {"message": "All orders cancelled"} 126 | 127 | 128 | @app.post("/close_all") 129 | async def close_all(): 130 | api.close_all() 131 | return {"message": "All positions closed"} 132 | 133 | 134 | @app.post("/logout") 135 | async def logout(): 136 | return {"message": api.logout()} 137 | 138 | 139 | @app.post("/status") 140 | async def status(): 141 | return {"connected": api.isconnected()} 142 | 143 | 144 | @app.post("/subscribe") 145 | async def subscribe(subscription: SymbolModel): 146 | for symbol in subscription.symbols: 147 | api.subscribe(symbol) 148 | return {"message": "Subscribed to symbols"} 149 | -------------------------------------------------------------------------------- /examples/restapi/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | ejtraderctrest: 4 | image: ejtrader/ejtraderct:rest 5 | container_name: ejtraderctfixrest 6 | restart: always 7 | build: 8 | context: . 9 | dockerfile: Dockerfile 10 | 11 | ports: 12 | - "8000:8000" 13 | env_file: 14 | - .env 15 | -------------------------------------------------------------------------------- /examples/restapi/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn 3 | pydantic 4 | ejtraderCT==1.1.3 -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: ejtraderCT 2 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | [MAIN] 2 | 3 | # Analyse import fallback blocks. This can be used to support both Python 2 and 4 | # 3 compatible code, which means that the block might have code that exists 5 | # only in one or another interpreter, leading to false positives when analysed. 6 | analyse-fallback-blocks=no 7 | 8 | # Clear in-memory caches upon conclusion of linting. Useful if running pylint 9 | # in a server-like mode. 10 | clear-cache-post-run=no 11 | 12 | # Load and enable all available extensions. Use --list-extensions to see a list 13 | # all available extensions. 14 | #enable-all-extensions= 15 | 16 | # In error mode, messages with a category besides ERROR or FATAL are 17 | # suppressed, and no reports are done by default. Error mode is compatible with 18 | # disabling specific errors. 19 | #errors-only= 20 | 21 | # Always return a 0 (non-error) status code, even if lint errors are found. 22 | # This is primarily useful in continuous integration scripts. 23 | #exit-zero= 24 | 25 | # A comma-separated list of package or module names from where C extensions may 26 | # be loaded. Extensions are loading into the active Python interpreter and may 27 | # run arbitrary code. 28 | extension-pkg-allow-list= 29 | 30 | # A comma-separated list of package or module names from where C extensions may 31 | # be loaded. Extensions are loading into the active Python interpreter and may 32 | # run arbitrary code. (This is an alternative name to extension-pkg-allow-list 33 | # for backward compatibility.) 34 | extension-pkg-whitelist= 35 | 36 | # Return non-zero exit code if any of these messages/categories are detected, 37 | # even if score is above --fail-under value. Syntax same as enable. Messages 38 | # specified are enabled, while categories only check already-enabled messages. 39 | fail-on= 40 | 41 | # Specify a score threshold under which the program will exit with error. 42 | fail-under=10 43 | 44 | # Interpret the stdin as a python script, whose filename needs to be passed as 45 | # the module_or_package argument. 46 | #from-stdin= 47 | 48 | # Files or directories to be skipped. They should be base names, not paths. 49 | ignore=CVS 50 | 51 | # Add files or directories matching the regular expressions patterns to the 52 | # ignore-list. The regex matches against paths and can be in Posix or Windows 53 | # format. Because '\\' represents the directory delimiter on Windows systems, 54 | # it can't be used as an escape character. 55 | ignore-paths= 56 | 57 | # Files or directories matching the regular expression patterns are skipped. 58 | # The regex matches against base names, not paths. The default value ignores 59 | # Emacs file locks 60 | ignore-patterns=^\.# 61 | 62 | # List of module names for which member attributes should not be checked 63 | # (useful for modules/projects where namespaces are manipulated during runtime 64 | # and thus existing member attributes cannot be deduced by static analysis). It 65 | # supports qualified module names, as well as Unix pattern matching. 66 | ignored-modules= 67 | 68 | # Python code to execute, usually for sys.path manipulation such as 69 | # pygtk.require(). 70 | #init-hook= 71 | 72 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 73 | # number of processors available to use, and will cap the count on Windows to 74 | # avoid hangs. 75 | jobs=1 76 | 77 | # Control the amount of potential inferred values when inferring a single 78 | # object. This can help the performance when dealing with large functions or 79 | # complex, nested conditions. 80 | limit-inference-results=100 81 | 82 | # List of plugins (as comma separated values of python module names) to load, 83 | # usually to register additional checkers. 84 | load-plugins= 85 | 86 | # Pickle collected data for later comparisons. 87 | persistent=yes 88 | 89 | # Minimum Python version to use for version dependent checks. Will default to 90 | # the version used to run pylint. 91 | py-version=3.9 92 | 93 | # Discover python modules and packages in the file system subtree. 94 | recursive=no 95 | 96 | # When enabled, pylint would attempt to guess common misconfiguration and emit 97 | # user-friendly hints instead of false-positive error messages. 98 | suggestion-mode=yes 99 | 100 | # Allow loading of arbitrary C extensions. Extensions are imported into the 101 | # active Python interpreter and may run arbitrary code. 102 | unsafe-load-any-extension=no 103 | 104 | # In verbose mode, extra non-checker-related info will be displayed. 105 | #verbose= 106 | 107 | 108 | [BASIC] 109 | 110 | # Naming style matching correct argument names. 111 | argument-naming-style=snake_case 112 | 113 | # Regular expression matching correct argument names. Overrides argument- 114 | # naming-style. If left empty, argument names will be checked with the set 115 | # naming style. 116 | #argument-rgx= 117 | 118 | # Naming style matching correct attribute names. 119 | attr-naming-style=snake_case 120 | 121 | # Regular expression matching correct attribute names. Overrides attr-naming- 122 | # style. If left empty, attribute names will be checked with the set naming 123 | # style. 124 | #attr-rgx= 125 | 126 | # Bad variable names which should always be refused, separated by a comma. 127 | bad-names=foo, 128 | bar, 129 | baz, 130 | toto, 131 | tutu, 132 | tata 133 | 134 | # Bad variable names regexes, separated by a comma. If names match any regex, 135 | # they will always be refused 136 | bad-names-rgxs= 137 | 138 | # Naming style matching correct class attribute names. 139 | class-attribute-naming-style=any 140 | 141 | # Regular expression matching correct class attribute names. Overrides class- 142 | # attribute-naming-style. If left empty, class attribute names will be checked 143 | # with the set naming style. 144 | #class-attribute-rgx= 145 | 146 | # Naming style matching correct class constant names. 147 | class-const-naming-style=UPPER_CASE 148 | 149 | # Regular expression matching correct class constant names. Overrides class- 150 | # const-naming-style. If left empty, class constant names will be checked with 151 | # the set naming style. 152 | #class-const-rgx= 153 | 154 | # Naming style matching correct class names. 155 | class-naming-style=PascalCase 156 | 157 | # Regular expression matching correct class names. Overrides class-naming- 158 | # style. If left empty, class names will be checked with the set naming style. 159 | #class-rgx= 160 | 161 | # Naming style matching correct constant names. 162 | const-naming-style=UPPER_CASE 163 | 164 | # Regular expression matching correct constant names. Overrides const-naming- 165 | # style. If left empty, constant names will be checked with the set naming 166 | # style. 167 | #const-rgx= 168 | 169 | # Minimum line length for functions/classes that require docstrings, shorter 170 | # ones are exempt. 171 | docstring-min-length=-1 172 | 173 | # Naming style matching correct function names. 174 | function-naming-style=snake_case 175 | 176 | # Regular expression matching correct function names. Overrides function- 177 | # naming-style. If left empty, function names will be checked with the set 178 | # naming style. 179 | #function-rgx= 180 | 181 | # Good variable names which should always be accepted, separated by a comma. 182 | good-names=i, 183 | j, 184 | k, 185 | ex, 186 | Run, 187 | _ 188 | 189 | # Good variable names regexes, separated by a comma. If names match any regex, 190 | # they will always be accepted 191 | good-names-rgxs= 192 | 193 | # Include a hint for the correct naming format with invalid-name. 194 | include-naming-hint=no 195 | 196 | # Naming style matching correct inline iteration names. 197 | inlinevar-naming-style=any 198 | 199 | # Regular expression matching correct inline iteration names. Overrides 200 | # inlinevar-naming-style. If left empty, inline iteration names will be checked 201 | # with the set naming style. 202 | #inlinevar-rgx= 203 | 204 | # Naming style matching correct method names. 205 | method-naming-style=snake_case 206 | 207 | # Regular expression matching correct method names. Overrides method-naming- 208 | # style. If left empty, method names will be checked with the set naming style. 209 | #method-rgx= 210 | 211 | # Naming style matching correct module names. 212 | module-naming-style=snake_case 213 | 214 | # Regular expression matching correct module names. Overrides module-naming- 215 | # style. If left empty, module names will be checked with the set naming style. 216 | #module-rgx= 217 | 218 | # Colon-delimited sets of names that determine each other's naming style when 219 | # the name regexes allow several styles. 220 | name-group= 221 | 222 | # Regular expression which should only match function or class names that do 223 | # not require a docstring. 224 | no-docstring-rgx=^_ 225 | 226 | # List of decorators that produce properties, such as abc.abstractproperty. Add 227 | # to this list to register other decorators that produce valid properties. 228 | # These decorators are taken in consideration only for invalid-name. 229 | property-classes=abc.abstractproperty 230 | 231 | # Regular expression matching correct type variable names. If left empty, type 232 | # variable names will be checked with the set naming style. 233 | #typevar-rgx= 234 | 235 | # Naming style matching correct variable names. 236 | variable-naming-style=snake_case 237 | 238 | # Regular expression matching correct variable names. Overrides variable- 239 | # naming-style. If left empty, variable names will be checked with the set 240 | # naming style. 241 | #variable-rgx= 242 | 243 | 244 | [CLASSES] 245 | 246 | # Warn about protected attribute access inside special methods 247 | check-protected-access-in-special-methods=no 248 | 249 | # List of method names used to declare (i.e. assign) instance attributes. 250 | defining-attr-methods=__init__, 251 | __new__, 252 | setUp, 253 | __post_init__ 254 | 255 | # List of member names, which should be excluded from the protected access 256 | # warning. 257 | exclude-protected=_asdict, 258 | _fields, 259 | _replace, 260 | _source, 261 | _make 262 | 263 | # List of valid names for the first argument in a class method. 264 | valid-classmethod-first-arg=cls 265 | 266 | # List of valid names for the first argument in a metaclass class method. 267 | valid-metaclass-classmethod-first-arg=mcs 268 | 269 | 270 | [DESIGN] 271 | 272 | # List of regular expressions of class ancestor names to ignore when counting 273 | # public methods (see R0903) 274 | exclude-too-few-public-methods= 275 | 276 | # List of qualified class names to ignore when counting class parents (see 277 | # R0901) 278 | ignored-parents= 279 | 280 | # Maximum number of arguments for function / method. 281 | max-args=5 282 | 283 | # Maximum number of attributes for a class (see R0902). 284 | max-attributes=7 285 | 286 | # Maximum number of boolean expressions in an if statement (see R0916). 287 | max-bool-expr=5 288 | 289 | # Maximum number of branch for function / method body. 290 | max-branches=12 291 | 292 | # Maximum number of locals for function / method body. 293 | max-locals=15 294 | 295 | # Maximum number of parents for a class (see R0901). 296 | max-parents=7 297 | 298 | # Maximum number of public methods for a class (see R0904). 299 | max-public-methods=20 300 | 301 | # Maximum number of return / yield for function / method body. 302 | max-returns=6 303 | 304 | # Maximum number of statements in function / method body. 305 | max-statements=50 306 | 307 | # Minimum number of public methods for a class (see R0903). 308 | min-public-methods=2 309 | 310 | 311 | [EXCEPTIONS] 312 | 313 | # Exceptions that will emit a warning when caught. 314 | overgeneral-exceptions=builtins.BaseException,builtins.Exception 315 | 316 | 317 | [FORMAT] 318 | 319 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 320 | expected-line-ending-format= 321 | 322 | # Regexp for a line that is allowed to be longer than the limit. 323 | ignore-long-lines=^\s*(# )??$ 324 | 325 | # Number of spaces of indent required inside a hanging or continued line. 326 | indent-after-paren=4 327 | 328 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 329 | # tab). 330 | indent-string=' ' 331 | 332 | # Maximum number of characters on a single line. 333 | max-line-length=100 334 | 335 | # Maximum number of lines in a module. 336 | max-module-lines=1000 337 | 338 | # Allow the body of a class to be on the same line as the declaration if body 339 | # contains single statement. 340 | single-line-class-stmt=no 341 | 342 | # Allow the body of an if to be on the same line as the test if there is no 343 | # else. 344 | single-line-if-stmt=no 345 | 346 | 347 | [IMPORTS] 348 | 349 | # List of modules that can be imported at any level, not just the top level 350 | # one. 351 | allow-any-import-level= 352 | 353 | # Allow explicit reexports by alias from a package __init__. 354 | allow-reexport-from-package=no 355 | 356 | # Allow wildcard imports from modules that define __all__. 357 | allow-wildcard-with-all=no 358 | 359 | # Deprecated modules which should not be used, separated by a comma. 360 | deprecated-modules= 361 | 362 | # Output a graph (.gv or any supported image format) of external dependencies 363 | # to the given file (report RP0402 must not be disabled). 364 | ext-import-graph= 365 | 366 | # Output a graph (.gv or any supported image format) of all (i.e. internal and 367 | # external) dependencies to the given file (report RP0402 must not be 368 | # disabled). 369 | import-graph= 370 | 371 | # Output a graph (.gv or any supported image format) of internal dependencies 372 | # to the given file (report RP0402 must not be disabled). 373 | int-import-graph= 374 | 375 | # Force import order to recognize a module as part of the standard 376 | # compatibility libraries. 377 | known-standard-library= 378 | 379 | # Force import order to recognize a module as part of a third party library. 380 | known-third-party=enchant 381 | 382 | # Couples of modules and preferred modules, separated by a comma. 383 | preferred-modules= 384 | 385 | 386 | [LOGGING] 387 | 388 | # The type of string formatting that logging methods do. `old` means using % 389 | # formatting, `new` is for `{}` formatting. 390 | logging-format-style=old 391 | 392 | # Logging modules to check that the string format arguments are in logging 393 | # function parameter format. 394 | logging-modules=logging 395 | 396 | 397 | [MESSAGES CONTROL] 398 | 399 | # Only show warnings with the listed confidence levels. Leave empty to show 400 | # all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, 401 | # UNDEFINED. 402 | confidence=HIGH, 403 | CONTROL_FLOW, 404 | INFERENCE, 405 | INFERENCE_FAILURE, 406 | UNDEFINED 407 | 408 | # Disable the message, report, category or checker with the given id(s). You 409 | # can either give multiple identifiers separated by comma (,) or put this 410 | # option multiple times (only on the command line, not in the configuration 411 | # file where it should appear only once). You can also use "--disable=all" to 412 | # disable everything first and then re-enable specific checks. For example, if 413 | # you want to run only the similarities checker, you can use "--disable=all 414 | # --enable=similarities". If you want to run only the classes checker, but have 415 | # no Warning level messages displayed, use "--disable=all --enable=classes 416 | # --disable=W". 417 | disable=raw-checker-failed, 418 | bad-inline-option, 419 | locally-disabled, 420 | file-ignored, 421 | suppressed-message, 422 | useless-suppression, 423 | deprecated-pragma, 424 | use-symbolic-message-instead, 425 | missing-function-docstring, 426 | missing-class-docstring,C, R, W, F, E,E501 427 | 428 | # Enable the message, report, category or checker with the given id(s). You can 429 | # either give multiple identifier separated by comma (,) or put this option 430 | # multiple time (only on the command line, not in the configuration file where 431 | # it should appear only once). See also the "--disable" option for examples. 432 | enable=c-extension-no-member 433 | 434 | 435 | [METHOD_ARGS] 436 | 437 | # List of qualified names (i.e., library.method) which require a timeout 438 | # parameter e.g. 'requests.api.get,requests.api.post' 439 | timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request 440 | 441 | 442 | [MISCELLANEOUS] 443 | 444 | # List of note tags to take in consideration, separated by a comma. 445 | notes=FIXME, 446 | XXX, 447 | TODO 448 | 449 | # Regular expression of note tags to take in consideration. 450 | notes-rgx= 451 | 452 | 453 | [REFACTORING] 454 | 455 | # Maximum number of nested blocks for function / method body 456 | max-nested-blocks=5 457 | 458 | # Complete name of functions that never returns. When checking for 459 | # inconsistent-return-statements if a never returning function is called then 460 | # it will be considered as an explicit return statement and no message will be 461 | # printed. 462 | never-returning-functions=sys.exit,argparse.parse_error 463 | 464 | 465 | [REPORTS] 466 | 467 | # Python expression which should return a score less than or equal to 10. You 468 | # have access to the variables 'fatal', 'error', 'warning', 'refactor', 469 | # 'convention', and 'info' which contain the number of messages in each 470 | # category, as well as 'statement' which is the total number of statements 471 | # analyzed. This score is used by the global evaluation report (RP0004). 472 | evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) 473 | 474 | # Template used to display messages. This is a python new-style format string 475 | # used to format the message information. See doc for all details. 476 | msg-template= 477 | 478 | # Set the output format. Available formats are text, parseable, colorized, json 479 | # and msvs (visual studio). You can also give a reporter class, e.g. 480 | # mypackage.mymodule.MyReporterClass. 481 | #output-format= 482 | 483 | # Tells whether to display a full report or only the messages. 484 | reports=no 485 | 486 | # Activate the evaluation score. 487 | score=yes 488 | 489 | 490 | [SIMILARITIES] 491 | 492 | # Comments are removed from the similarity computation 493 | ignore-comments=yes 494 | 495 | # Docstrings are removed from the similarity computation 496 | ignore-docstrings=yes 497 | 498 | # Imports are removed from the similarity computation 499 | ignore-imports=yes 500 | 501 | # Signatures are removed from the similarity computation 502 | ignore-signatures=yes 503 | 504 | # Minimum lines number of a similarity. 505 | min-similarity-lines=4 506 | 507 | 508 | [SPELLING] 509 | 510 | # Limits count of emitted suggestions for spelling mistakes. 511 | max-spelling-suggestions=4 512 | 513 | # Spelling dictionary name. Available dictionaries: none. To make it work, 514 | # install the 'python-enchant' package. 515 | spelling-dict= 516 | 517 | # List of comma separated words that should be considered directives if they 518 | # appear at the beginning of a comment and should not be checked. 519 | spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: 520 | 521 | # List of comma separated words that should not be checked. 522 | spelling-ignore-words= 523 | 524 | # A path to a file that contains the private dictionary; one word per line. 525 | spelling-private-dict-file= 526 | 527 | # Tells whether to store unknown words to the private dictionary (see the 528 | # --spelling-private-dict-file option) instead of raising a message. 529 | spelling-store-unknown-words=no 530 | 531 | 532 | [STRING] 533 | 534 | # This flag controls whether inconsistent-quotes generates a warning when the 535 | # character used as a quote delimiter is used inconsistently within a module. 536 | check-quote-consistency=no 537 | 538 | # This flag controls whether the implicit-str-concat should generate a warning 539 | # on implicit string concatenation in sequences defined over several lines. 540 | check-str-concat-over-line-jumps=no 541 | 542 | 543 | [TYPECHECK] 544 | 545 | # List of decorators that produce context managers, such as 546 | # contextlib.contextmanager. Add to this list to register other decorators that 547 | # produce valid context managers. 548 | contextmanager-decorators=contextlib.contextmanager 549 | 550 | # List of members which are set dynamically and missed by pylint inference 551 | # system, and so shouldn't trigger E1101 when accessed. Python regular 552 | # expressions are accepted. 553 | generated-members= 554 | 555 | # Tells whether to warn about missing members when the owner of the attribute 556 | # is inferred to be None. 557 | ignore-none=yes 558 | 559 | # This flag controls whether pylint should warn about no-member and similar 560 | # checks whenever an opaque object is returned when inferring. The inference 561 | # can return multiple potential results while evaluating a Python object, but 562 | # some branches might not be evaluated, which results in partial inference. In 563 | # that case, it might be useful to still emit no-member and other checks for 564 | # the rest of the inferred objects. 565 | ignore-on-opaque-inference=yes 566 | 567 | # List of symbolic message names to ignore for Mixin members. 568 | ignored-checks-for-mixins=no-member, 569 | not-async-context-manager, 570 | not-context-manager, 571 | attribute-defined-outside-init 572 | 573 | # List of class names for which member attributes should not be checked (useful 574 | # for classes with dynamically set attributes). This supports the use of 575 | # qualified names. 576 | ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace 577 | 578 | # Show a hint with possible names when a member name was not found. The aspect 579 | # of finding the hint is based on edit distance. 580 | missing-member-hint=yes 581 | 582 | # The minimum edit distance a name should have in order to be considered a 583 | # similar match for a missing member name. 584 | missing-member-hint-distance=1 585 | 586 | # The total number of similar names that should be taken in consideration when 587 | # showing a hint for a missing member. 588 | missing-member-max-choices=1 589 | 590 | # Regex pattern to define which classes are considered mixins. 591 | mixin-class-rgx=.*[Mm]ixin 592 | 593 | # List of decorators that change the signature of a decorated function. 594 | signature-mutators= 595 | 596 | 597 | [VARIABLES] 598 | 599 | # List of additional names supposed to be defined in builtins. Remember that 600 | # you should avoid defining new builtins when possible. 601 | additional-builtins= 602 | 603 | # Tells whether unused global variables should be treated as a violation. 604 | allow-global-unused-variables=yes 605 | 606 | # List of names allowed to shadow builtins 607 | allowed-redefined-builtins= 608 | 609 | # List of strings which can identify a callback function by name. A callback 610 | # name must start or end with one of those strings. 611 | callbacks=cb_, 612 | _cb 613 | 614 | # A regular expression matching the name of dummy variables (i.e. expected to 615 | # not be used). 616 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 617 | 618 | # Argument names that match this expression will be ignored. 619 | ignored-argument-names=_.*|^ignored_|^unused_ 620 | 621 | # Tells whether we should check for unused import in __init__ files. 622 | init-import=no 623 | 624 | # List of qualified module names which can have objects that can redefine 625 | # builtins. 626 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 627 | -------------------------------------------------------------------------------- /pylintrcconda: -------------------------------------------------------------------------------- 1 | [MAIN] 2 | 3 | # Analyse import fallback blocks. This can be used to support both Python 2 and 4 | # 3 compatible code, which means that the block might have code that exists 5 | # only in one or another interpreter, leading to false positives when analysed. 6 | analyse-fallback-blocks=no 7 | 8 | # Clear in-memory caches upon conclusion of linting. Useful if running pylint 9 | # in a server-like mode. 10 | clear-cache-post-run=no 11 | 12 | # Load and enable all available extensions. Use --list-extensions to see a list 13 | # all available extensions. 14 | #enable-all-extensions= 15 | 16 | # In error mode, messages with a category besides ERROR or FATAL are 17 | # suppressed, and no reports are done by default. Error mode is compatible with 18 | # disabling specific errors. 19 | #errors-only= 20 | 21 | # Always return a 0 (non-error) status code, even if lint errors are found. 22 | # This is primarily useful in continuous integration scripts. 23 | #exit-zero= 24 | 25 | # A comma-separated list of package or module names from where C extensions may 26 | # be loaded. Extensions are loading into the active Python interpreter and may 27 | # run arbitrary code. 28 | extension-pkg-allow-list= 29 | 30 | # A comma-separated list of package or module names from where C extensions may 31 | # be loaded. Extensions are loading into the active Python interpreter and may 32 | # run arbitrary code. (This is an alternative name to extension-pkg-allow-list 33 | # for backward compatibility.) 34 | extension-pkg-whitelist= 35 | 36 | # Return non-zero exit code if any of these messages/categories are detected, 37 | # even if score is above --fail-under value. Syntax same as enable. Messages 38 | # specified are enabled, while categories only check already-enabled messages. 39 | fail-on= 40 | 41 | # Specify a score threshold under which the program will exit with error. 42 | fail-under=10 43 | 44 | # Interpret the stdin as a python script, whose filename needs to be passed as 45 | # the module_or_package argument. 46 | #from-stdin= 47 | 48 | # Files or directories to be skipped. They should be base names, not paths. 49 | ignore=CVS 50 | 51 | # Add files or directories matching the regular expressions patterns to the 52 | # ignore-list. The regex matches against paths and can be in Posix or Windows 53 | # format. Because '\\' represents the directory delimiter on Windows systems, 54 | # it can't be used as an escape character. 55 | ignore-paths= 56 | 57 | # Files or directories matching the regular expression patterns are skipped. 58 | # The regex matches against base names, not paths. The default value ignores 59 | # Emacs file locks 60 | ignore-patterns=^\.# 61 | 62 | # List of module names for which member attributes should not be checked 63 | # (useful for modules/projects where namespaces are manipulated during runtime 64 | # and thus existing member attributes cannot be deduced by static analysis). It 65 | # supports qualified module names, as well as Unix pattern matching. 66 | ignored-modules= 67 | 68 | # Python code to execute, usually for sys.path manipulation such as 69 | # pygtk.require(). 70 | #init-hook= 71 | 72 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 73 | # number of processors available to use, and will cap the count on Windows to 74 | # avoid hangs. 75 | jobs=1 76 | 77 | # Control the amount of potential inferred values when inferring a single 78 | # object. This can help the performance when dealing with large functions or 79 | # complex, nested conditions. 80 | limit-inference-results=100 81 | 82 | # List of plugins (as comma separated values of python module names) to load, 83 | # usually to register additional checkers. 84 | load-plugins= 85 | 86 | # Pickle collected data for later comparisons. 87 | persistent=yes 88 | 89 | # Minimum Python version to use for version dependent checks. Will default to 90 | # the version used to run pylint. 91 | py-version=3.9 92 | 93 | # Discover python modules and packages in the file system subtree. 94 | recursive=no 95 | 96 | # When enabled, pylint would attempt to guess common misconfiguration and emit 97 | # user-friendly hints instead of false-positive error messages. 98 | suggestion-mode=yes 99 | 100 | # Allow loading of arbitrary C extensions. Extensions are imported into the 101 | # active Python interpreter and may run arbitrary code. 102 | unsafe-load-any-extension=no 103 | 104 | # In verbose mode, extra non-checker-related info will be displayed. 105 | #verbose= 106 | 107 | 108 | [BASIC] 109 | 110 | # Naming style matching correct argument names. 111 | argument-naming-style=snake_case 112 | 113 | # Regular expression matching correct argument names. Overrides argument- 114 | # naming-style. If left empty, argument names will be checked with the set 115 | # naming style. 116 | #argument-rgx= 117 | 118 | # Naming style matching correct attribute names. 119 | attr-naming-style=snake_case 120 | 121 | # Regular expression matching correct attribute names. Overrides attr-naming- 122 | # style. If left empty, attribute names will be checked with the set naming 123 | # style. 124 | #attr-rgx= 125 | 126 | # Bad variable names which should always be refused, separated by a comma. 127 | bad-names=foo, 128 | bar, 129 | baz, 130 | toto, 131 | tutu, 132 | tata 133 | 134 | # Bad variable names regexes, separated by a comma. If names match any regex, 135 | # they will always be refused 136 | bad-names-rgxs= 137 | 138 | # Naming style matching correct class attribute names. 139 | class-attribute-naming-style=any 140 | 141 | # Regular expression matching correct class attribute names. Overrides class- 142 | # attribute-naming-style. If left empty, class attribute names will be checked 143 | # with the set naming style. 144 | #class-attribute-rgx= 145 | 146 | # Naming style matching correct class constant names. 147 | class-const-naming-style=UPPER_CASE 148 | 149 | # Regular expression matching correct class constant names. Overrides class- 150 | # const-naming-style. If left empty, class constant names will be checked with 151 | # the set naming style. 152 | #class-const-rgx= 153 | 154 | # Naming style matching correct class names. 155 | class-naming-style=PascalCase 156 | 157 | # Regular expression matching correct class names. Overrides class-naming- 158 | # style. If left empty, class names will be checked with the set naming style. 159 | #class-rgx= 160 | 161 | # Naming style matching correct constant names. 162 | const-naming-style=UPPER_CASE 163 | 164 | # Regular expression matching correct constant names. Overrides const-naming- 165 | # style. If left empty, constant names will be checked with the set naming 166 | # style. 167 | #const-rgx= 168 | 169 | # Minimum line length for functions/classes that require docstrings, shorter 170 | # ones are exempt. 171 | docstring-min-length=-1 172 | 173 | # Naming style matching correct function names. 174 | function-naming-style=snake_case 175 | 176 | # Regular expression matching correct function names. Overrides function- 177 | # naming-style. If left empty, function names will be checked with the set 178 | # naming style. 179 | #function-rgx= 180 | 181 | # Good variable names which should always be accepted, separated by a comma. 182 | good-names=i, 183 | j, 184 | k, 185 | ex, 186 | Run, 187 | _ 188 | 189 | # Good variable names regexes, separated by a comma. If names match any regex, 190 | # they will always be accepted 191 | good-names-rgxs= 192 | 193 | # Include a hint for the correct naming format with invalid-name. 194 | include-naming-hint=no 195 | 196 | # Naming style matching correct inline iteration names. 197 | inlinevar-naming-style=any 198 | 199 | # Regular expression matching correct inline iteration names. Overrides 200 | # inlinevar-naming-style. If left empty, inline iteration names will be checked 201 | # with the set naming style. 202 | #inlinevar-rgx= 203 | 204 | # Naming style matching correct method names. 205 | method-naming-style=snake_case 206 | 207 | # Regular expression matching correct method names. Overrides method-naming- 208 | # style. If left empty, method names will be checked with the set naming style. 209 | #method-rgx= 210 | 211 | # Naming style matching correct module names. 212 | module-naming-style=snake_case 213 | 214 | # Regular expression matching correct module names. Overrides module-naming- 215 | # style. If left empty, module names will be checked with the set naming style. 216 | #module-rgx= 217 | 218 | # Colon-delimited sets of names that determine each other's naming style when 219 | # the name regexes allow several styles. 220 | name-group= 221 | 222 | # Regular expression which should only match function or class names that do 223 | # not require a docstring. 224 | no-docstring-rgx=^_ 225 | 226 | # List of decorators that produce properties, such as abc.abstractproperty. Add 227 | # to this list to register other decorators that produce valid properties. 228 | # These decorators are taken in consideration only for invalid-name. 229 | property-classes=abc.abstractproperty 230 | 231 | # Regular expression matching correct type variable names. If left empty, type 232 | # variable names will be checked with the set naming style. 233 | #typevar-rgx= 234 | 235 | # Naming style matching correct variable names. 236 | variable-naming-style=snake_case 237 | 238 | # Regular expression matching correct variable names. Overrides variable- 239 | # naming-style. If left empty, variable names will be checked with the set 240 | # naming style. 241 | #variable-rgx= 242 | 243 | 244 | [CLASSES] 245 | 246 | # Warn about protected attribute access inside special methods 247 | check-protected-access-in-special-methods=no 248 | 249 | # List of method names used to declare (i.e. assign) instance attributes. 250 | defining-attr-methods=__init__, 251 | __new__, 252 | setUp, 253 | __post_init__ 254 | 255 | # List of member names, which should be excluded from the protected access 256 | # warning. 257 | exclude-protected=_asdict, 258 | _fields, 259 | _replace, 260 | _source, 261 | _make 262 | 263 | # List of valid names for the first argument in a class method. 264 | valid-classmethod-first-arg=cls 265 | 266 | # List of valid names for the first argument in a metaclass class method. 267 | valid-metaclass-classmethod-first-arg=mcs 268 | 269 | 270 | [DESIGN] 271 | 272 | # List of regular expressions of class ancestor names to ignore when counting 273 | # public methods (see R0903) 274 | exclude-too-few-public-methods= 275 | 276 | # List of qualified class names to ignore when counting class parents (see 277 | # R0901) 278 | ignored-parents= 279 | 280 | # Maximum number of arguments for function / method. 281 | max-args=5 282 | 283 | # Maximum number of attributes for a class (see R0902). 284 | max-attributes=7 285 | 286 | # Maximum number of boolean expressions in an if statement (see R0916). 287 | max-bool-expr=5 288 | 289 | # Maximum number of branch for function / method body. 290 | max-branches=12 291 | 292 | # Maximum number of locals for function / method body. 293 | max-locals=15 294 | 295 | # Maximum number of parents for a class (see R0901). 296 | max-parents=7 297 | 298 | # Maximum number of public methods for a class (see R0904). 299 | max-public-methods=20 300 | 301 | # Maximum number of return / yield for function / method body. 302 | max-returns=6 303 | 304 | # Maximum number of statements in function / method body. 305 | max-statements=50 306 | 307 | # Minimum number of public methods for a class (see R0903). 308 | min-public-methods=2 309 | 310 | 311 | [EXCEPTIONS] 312 | 313 | # Exceptions that will emit a warning when caught. 314 | overgeneral-exceptions=builtins.BaseException,builtins.Exception 315 | 316 | 317 | [FORMAT] 318 | 319 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 320 | expected-line-ending-format= 321 | 322 | # Regexp for a line that is allowed to be longer than the limit. 323 | ignore-long-lines=^\s*(# )??$ 324 | 325 | # Number of spaces of indent required inside a hanging or continued line. 326 | indent-after-paren=4 327 | 328 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 329 | # tab). 330 | indent-string=' ' 331 | 332 | # Maximum number of characters on a single line. 333 | max-line-length=100 334 | 335 | # Maximum number of lines in a module. 336 | max-module-lines=1000 337 | 338 | # Allow the body of a class to be on the same line as the declaration if body 339 | # contains single statement. 340 | single-line-class-stmt=no 341 | 342 | # Allow the body of an if to be on the same line as the test if there is no 343 | # else. 344 | single-line-if-stmt=no 345 | 346 | 347 | [IMPORTS] 348 | 349 | # List of modules that can be imported at any level, not just the top level 350 | # one. 351 | allow-any-import-level= 352 | 353 | # Allow explicit reexports by alias from a package __init__. 354 | allow-reexport-from-package=no 355 | 356 | # Allow wildcard imports from modules that define __all__. 357 | allow-wildcard-with-all=no 358 | 359 | # Deprecated modules which should not be used, separated by a comma. 360 | deprecated-modules= 361 | 362 | # Output a graph (.gv or any supported image format) of external dependencies 363 | # to the given file (report RP0402 must not be disabled). 364 | ext-import-graph= 365 | 366 | # Output a graph (.gv or any supported image format) of all (i.e. internal and 367 | # external) dependencies to the given file (report RP0402 must not be 368 | # disabled). 369 | import-graph= 370 | 371 | # Output a graph (.gv or any supported image format) of internal dependencies 372 | # to the given file (report RP0402 must not be disabled). 373 | int-import-graph= 374 | 375 | # Force import order to recognize a module as part of the standard 376 | # compatibility libraries. 377 | known-standard-library= 378 | 379 | # Force import order to recognize a module as part of a third party library. 380 | known-third-party=enchant 381 | 382 | # Couples of modules and preferred modules, separated by a comma. 383 | preferred-modules= 384 | 385 | 386 | [LOGGING] 387 | 388 | # The type of string formatting that logging methods do. `old` means using % 389 | # formatting, `new` is for `{}` formatting. 390 | logging-format-style=old 391 | 392 | # Logging modules to check that the string format arguments are in logging 393 | # function parameter format. 394 | logging-modules=logging 395 | 396 | 397 | [MESSAGES CONTROL] 398 | 399 | # Only show warnings with the listed confidence levels. Leave empty to show 400 | # all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, 401 | # UNDEFINED. 402 | confidence=HIGH, 403 | CONTROL_FLOW, 404 | INFERENCE, 405 | INFERENCE_FAILURE, 406 | UNDEFINED 407 | 408 | # Disable the message, report, category or checker with the given id(s). You 409 | # can either give multiple identifiers separated by comma (,) or put this 410 | # option multiple times (only on the command line, not in the configuration 411 | # file where it should appear only once). You can also use "--disable=all" to 412 | # disable everything first and then re-enable specific checks. For example, if 413 | # you want to run only the similarities checker, you can use "--disable=all 414 | # --enable=similarities". If you want to run only the classes checker, but have 415 | # no Warning level messages displayed, use "--disable=all --enable=classes 416 | # --disable=W". 417 | disable=raw-checker-failed, 418 | bad-inline-option, 419 | locally-disabled, 420 | file-ignored, 421 | suppressed-message, 422 | useless-suppression, 423 | deprecated-pragma, 424 | use-symbolic-message-instead, 425 | missing-function-docstring, 426 | missing-class-docstring 427 | 428 | # Enable the message, report, category or checker with the given id(s). You can 429 | # either give multiple identifier separated by comma (,) or put this option 430 | # multiple time (only on the command line, not in the configuration file where 431 | # it should appear only once). See also the "--disable" option for examples. 432 | enable=c-extension-no-member 433 | 434 | 435 | [METHOD_ARGS] 436 | 437 | # List of qualified names (i.e., library.method) which require a timeout 438 | # parameter e.g. 'requests.api.get,requests.api.post' 439 | timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request 440 | 441 | 442 | [MISCELLANEOUS] 443 | 444 | # List of note tags to take in consideration, separated by a comma. 445 | notes=FIXME, 446 | XXX, 447 | TODO 448 | 449 | # Regular expression of note tags to take in consideration. 450 | notes-rgx= 451 | 452 | 453 | [REFACTORING] 454 | 455 | # Maximum number of nested blocks for function / method body 456 | max-nested-blocks=5 457 | 458 | # Complete name of functions that never returns. When checking for 459 | # inconsistent-return-statements if a never returning function is called then 460 | # it will be considered as an explicit return statement and no message will be 461 | # printed. 462 | never-returning-functions=sys.exit,argparse.parse_error 463 | 464 | 465 | [REPORTS] 466 | 467 | # Python expression which should return a score less than or equal to 10. You 468 | # have access to the variables 'fatal', 'error', 'warning', 'refactor', 469 | # 'convention', and 'info' which contain the number of messages in each 470 | # category, as well as 'statement' which is the total number of statements 471 | # analyzed. This score is used by the global evaluation report (RP0004). 472 | evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) 473 | 474 | # Template used to display messages. This is a python new-style format string 475 | # used to format the message information. See doc for all details. 476 | msg-template= 477 | 478 | # Set the output format. Available formats are text, parseable, colorized, json 479 | # and msvs (visual studio). You can also give a reporter class, e.g. 480 | # mypackage.mymodule.MyReporterClass. 481 | #output-format= 482 | 483 | # Tells whether to display a full report or only the messages. 484 | reports=no 485 | 486 | # Activate the evaluation score. 487 | score=yes 488 | 489 | 490 | [SIMILARITIES] 491 | 492 | # Comments are removed from the similarity computation 493 | ignore-comments=yes 494 | 495 | # Docstrings are removed from the similarity computation 496 | ignore-docstrings=yes 497 | 498 | # Imports are removed from the similarity computation 499 | ignore-imports=yes 500 | 501 | # Signatures are removed from the similarity computation 502 | ignore-signatures=yes 503 | 504 | # Minimum lines number of a similarity. 505 | min-similarity-lines=4 506 | 507 | 508 | [SPELLING] 509 | 510 | # Limits count of emitted suggestions for spelling mistakes. 511 | max-spelling-suggestions=4 512 | 513 | # Spelling dictionary name. Available dictionaries: none. To make it work, 514 | # install the 'python-enchant' package. 515 | spelling-dict= 516 | 517 | # List of comma separated words that should be considered directives if they 518 | # appear at the beginning of a comment and should not be checked. 519 | spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: 520 | 521 | # List of comma separated words that should not be checked. 522 | spelling-ignore-words= 523 | 524 | # A path to a file that contains the private dictionary; one word per line. 525 | spelling-private-dict-file= 526 | 527 | # Tells whether to store unknown words to the private dictionary (see the 528 | # --spelling-private-dict-file option) instead of raising a message. 529 | spelling-store-unknown-words=no 530 | 531 | 532 | [STRING] 533 | 534 | # This flag controls whether inconsistent-quotes generates a warning when the 535 | # character used as a quote delimiter is used inconsistently within a module. 536 | check-quote-consistency=no 537 | 538 | # This flag controls whether the implicit-str-concat should generate a warning 539 | # on implicit string concatenation in sequences defined over several lines. 540 | check-str-concat-over-line-jumps=no 541 | 542 | 543 | [TYPECHECK] 544 | 545 | # List of decorators that produce context managers, such as 546 | # contextlib.contextmanager. Add to this list to register other decorators that 547 | # produce valid context managers. 548 | contextmanager-decorators=contextlib.contextmanager 549 | 550 | # List of members which are set dynamically and missed by pylint inference 551 | # system, and so shouldn't trigger E1101 when accessed. Python regular 552 | # expressions are accepted. 553 | generated-members= 554 | 555 | # Tells whether to warn about missing members when the owner of the attribute 556 | # is inferred to be None. 557 | ignore-none=yes 558 | 559 | # This flag controls whether pylint should warn about no-member and similar 560 | # checks whenever an opaque object is returned when inferring. The inference 561 | # can return multiple potential results while evaluating a Python object, but 562 | # some branches might not be evaluated, which results in partial inference. In 563 | # that case, it might be useful to still emit no-member and other checks for 564 | # the rest of the inferred objects. 565 | ignore-on-opaque-inference=yes 566 | 567 | # List of symbolic message names to ignore for Mixin members. 568 | ignored-checks-for-mixins=no-member, 569 | not-async-context-manager, 570 | not-context-manager, 571 | attribute-defined-outside-init 572 | 573 | # List of class names for which member attributes should not be checked (useful 574 | # for classes with dynamically set attributes). This supports the use of 575 | # qualified names. 576 | ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace 577 | 578 | # Show a hint with possible names when a member name was not found. The aspect 579 | # of finding the hint is based on edit distance. 580 | missing-member-hint=yes 581 | 582 | # The minimum edit distance a name should have in order to be considered a 583 | # similar match for a missing member name. 584 | missing-member-hint-distance=1 585 | 586 | # The total number of similar names that should be taken in consideration when 587 | # showing a hint for a missing member. 588 | missing-member-max-choices=1 589 | 590 | # Regex pattern to define which classes are considered mixins. 591 | mixin-class-rgx=.*[Mm]ixin 592 | 593 | # List of decorators that change the signature of a decorated function. 594 | signature-mutators= 595 | 596 | 597 | [VARIABLES] 598 | 599 | # List of additional names supposed to be defined in builtins. Remember that 600 | # you should avoid defining new builtins when possible. 601 | additional-builtins= 602 | 603 | # Tells whether unused global variables should be treated as a violation. 604 | allow-global-unused-variables=yes 605 | 606 | # List of names allowed to shadow builtins 607 | allowed-redefined-builtins= 608 | 609 | # List of strings which can identify a callback function by name. A callback 610 | # name must start or end with one of those strings. 611 | callbacks=cb_, 612 | _cb 613 | 614 | # A regular expression matching the name of dummy variables (i.e. expected to 615 | # not be used). 616 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 617 | 618 | # Argument names that match this expression will be ignored. 619 | ignored-argument-names=_.*|^ignored_|^unused_ 620 | 621 | # Tells whether we should check for unused import in __init__ files. 622 | init-import=no 623 | 624 | # List of qualified module names which can have objects that can redefine 625 | # builtins. 626 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 627 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ejtraderLabs/ejtraderCT/2358ca90bad156a9d82e38a06ff21c7d57d50b8c/requirements.txt -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ejtraderLabs/ejtraderCT/2358ca90bad156a9d82e38a06ff21c7d57d50b8c/setup.cfg -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import io 2 | 3 | from setuptools import setup, find_packages 4 | 5 | 6 | def readme(): 7 | with io.open("README.md", encoding="utf-8") as f: 8 | return f.read() 9 | 10 | 11 | def requirements(filename): 12 | reqs = list() 13 | with io.open(filename, encoding="utf-8") as f: 14 | for line in f.readlines(): 15 | reqs.append(line.strip()) 16 | return reqs 17 | 18 | 19 | # Version: 1.1.13 20 | setup( 21 | name="ejtraderCT", 22 | version="1.1.13", 23 | packages=find_packages(), 24 | url="https://ejtraderCT.readthedocs.io/", 25 | download_url="https://ejtrader.com", 26 | license="MIT License", 27 | author="Emerson Pedroso & Douglas Barros", 28 | author_email="support@ejtrader.com", 29 | description="Ctrader Fix API", 30 | long_description=readme(), 31 | long_description_content_type="text/markdown", 32 | install_requires=requirements(filename="requirements.txt"), 33 | include_package_data=True, 34 | classifiers=[ 35 | "Programming Language :: Python :: 3 :: Only", 36 | "Programming Language :: Python :: 3.5", 37 | "Programming Language :: Python :: 3.6", 38 | "Programming Language :: Python :: 3.7", 39 | "Programming Language :: Python :: 3.8", 40 | "License :: OSI Approved :: MIT License", 41 | "Intended Audience :: Developers", 42 | "Topic :: Office/Business :: Financial", 43 | "Topic :: Office/Business :: Financial :: Investment", 44 | "Topic :: Scientific/Engineering :: Information Analysis", 45 | "Topic :: Software Development :: Libraries", 46 | ], 47 | python_requires=">=3", 48 | keywords=", ".join( 49 | [ 50 | "ctrader", 51 | "fix-api", 52 | "historical-data", 53 | "financial-data", 54 | "stocks", 55 | "funds", 56 | "etfs", 57 | "indices", 58 | "currency crosses", 59 | "bonds", 60 | "commodities", 61 | "crypto currencies", 62 | ] 63 | ), 64 | project_urls={ 65 | "Bug Reports": "https://github.com/ejtraderLabs/ejtraderCT/issues", 66 | "Source": "https://github.com/ejtraderLabs/ejtraderCT", 67 | "Documentation": "https://ejtraderCT.readthedocs.io/", 68 | }, 69 | ) 70 | -------------------------------------------------------------------------------- /test/fixTraderTest.py: -------------------------------------------------------------------------------- 1 | 2 | import time 3 | import logging 4 | from datetime import datetime 5 | 6 | 7 | from ejtraderCT import Ctrader 8 | 9 | SERVER="h8.p.c-trader.cn" 10 | BROKER="icmarkets" 11 | LOGIN="3152339" 12 | PASSWORD="393214" 13 | CURRENCY="EUR" 14 | 15 | 16 | logging.getLogger().setLevel(logging.INFO) 17 | 18 | 19 | api = Ctrader(SERVER,BROKER,LOGIN,PASSWORD,CURRENCY) 20 | # 21 | # accountInfo = api.accountInfo() 22 | # print(accountInfo) 23 | # print(accountInfo['broker']) 24 | # print(accountInfo['balance']) 25 | 26 | #time.sleep(1) 27 | 28 | clid_buy = api.buy("GBPUSD", 0.01, 1.18, 1.19) 29 | clid_sell = api.sell("GBPUSD", 0.01, 1.19, 1.18) 30 | 31 | api.buyLimit("GBPUSD", 0.01, 1.17, 1.19, 1.18) 32 | api.sellLimit("GBPUSD", 0.01, 1.23, 1.17, 1.22) 33 | api.buyStop("GBPUSD", 0.01, 1.20, 1.24, 1.22) 34 | api.sellStop("GBPUSD", 0.01, 1.19, 1.17, 1.18) 35 | 36 | #time.sleep(1) 37 | 38 | #orders = api.orders() 39 | #print(orders) 40 | # for order in orders: 41 | # api.orderCancelById(order['ord_id']) 42 | 43 | time.sleep(1) 44 | 45 | #orders = api.orders() 46 | #print(orders) 47 | 48 | #positions = api.positions() 49 | #print(positions) 50 | 51 | position_buy = api.getPositionIdByClientId(clid_buy) 52 | position_sell = api.getPositionIdByClientId(clid_sell) 53 | 54 | print('buy gain ' + position_buy['gain']) 55 | print('sell gain ' + position_sell['gain']) 56 | 57 | api.close_all() 58 | api.cancel_all() 59 | 60 | # history = api.history("GBPUSD", "M5", int(datetime.now().timestamp()) - 10000) 61 | # print(history) -------------------------------------------------------------------------------- /test/symbol_table.py: -------------------------------------------------------------------------------- 1 | from ejtraderCT import SYMBOLSLIST 2 | 3 | symbol_table = SYMBOLSLIST['default'] 4 | 5 | print(symbol_table[int(1)]['pip_position']) -------------------------------------------------------------------------------- /test/test_math.py: -------------------------------------------------------------------------------- 1 | from ejtraderCT import calculate_spread, calculate_pip_value, calculate_commission 2 | 3 | 4 | def test_spread(): 5 | assert calculate_spread('113', '113.015', 2) == 15 6 | assert calculate_spread('1.09553', '1.09553', 4) == 0 7 | assert calculate_spread('9.59', '10', 1) == 41 8 | assert calculate_spread('113.1', '113.2', 2) == 100 9 | 10 | 11 | def test_pip_value(): 12 | assert calculate_pip_value('19.00570', 100000, 4) == '0.52616' 13 | assert calculate_pip_value('1.3348', 100000, 4) == '7.49176' 14 | assert calculate_pip_value('112.585', 10000, 2) == '0.88822' 15 | 16 | 17 | def test_commission(): 18 | assert calculate_commission(10000, 1, 0.000030) == 0.6 19 | 20 | 21 | --------------------------------------------------------------------------------