├── tutorials └── whook-AWS-EC2_linux_ENG.pdf ├── .gitignore ├── LICENSE ├── README.md └── main.py /tutorials/whook-AWS-EC2_linux_ENG.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/germangar/whook/HEAD/tutorials/whook-AWS-EC2_linux_ENG.pdf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | webhook.log 2 | bitget.log 3 | bybit.log 4 | bybitdemo.log 5 | bybitdemos1.log 6 | coinex.log 7 | coinexs1.log 8 | kucoin.log 9 | phemex.log 10 | phemexdemo.log 11 | winhook.rdp 12 | accounts.json 13 | accounts_back.json 14 | MENSAJES KUCOIN.txt 15 | binance_markets.log 16 | backup 17 | __pycache__ 18 | ngrok.exe 19 | bingx.log 20 | bybitdemos2.log 21 | historic.py 22 | binancedemo.log 23 | krakendemo.log 24 | Insomnia.Core-2023.2.2-portable.exe 25 | okxdemo.log 26 | fetch_all.py 27 | stuff/* 28 | logs/* 29 | okxdemos1.log 30 | okx.log 31 | kucoin1.log 32 | okx1.log 33 | config.json 34 | bitmart.log 35 | ascendex.log 36 | gserver.rdp 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Germán García Fernández 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # whook 2 | 3 | WHOOK is a web hook for handling Tradingview Alerts to crypto exchanges in perpetual USDT futures. 4 | 5 | Whook prioritizes reliability over speed. If you're looking for high frequency trading Whook is not for you. 6 | Whook will do everything it can to fullfill orders, including resending rejected orders until they time out (currently 40 seconds), reducing the quantity of the order when the balance is not enough and dividing the order in two at reversing positions when there's not enough balance for doing it at once. 7 | 8 | Whook only makes market orders and limit orders. Take profit and stop loss are not supported.
9 | Whook only uses one-side mode. Hedge mode is not supported.
10 | 11 | You don't need to be a programmer nor know how to clone a repository to use Whook. All you need is to download the **main.py** file and follow the instructions below. 12 | 13 | ##### Disclaimer: This project is for my personal use. I'm not taking feature requests. 14 | 15 | Currently supported exchanges: 16 | - **Kucoin** futures 17 | - **Bitget** futures 18 | - **Coinex** futures 19 | - **Bingx** 20 | - **OKX** futures ( also its demo mode ) [1] 21 | - **Bybit** futures ( also [Bybit testnet](https://testnet.bybit.com) ) 22 | - **Binance** futures ( also [Binance futures testnet](https://testnet.binancefuture.com) ) [2] 23 | - **Phemex** futures ( also [Phemex testnet](https://testnet.phemex.com) ) 24 | - **Kraken** futures ( also [Kraken futures testnet](https://demo-futures.kraken.com) ) 25 | 26 | I have been mostly using Bitget from the last six months. If anything has changed in the other exchanges APIs I will probably have missed it. If you experience any bugs report them in 'Issues' and I'll fix them in a few days. 27 | 28 | [1] I don't have access to OKX futures anymore (being European problems) so I can't guarantee it still works nor fix it if it doesn't. 29 | 30 | [2] Binance has stopped supporting futures in their testnet. They have now demo trading in their main platform. Unfortunately they require a Binance futures account for accesing demo trading, and being from Europe I can't have a Binance Futures account. In short: I can't maintain Binance anymore. I'll let it be there, just like OKX, but I can't guarantee they work nor fix them if they don't. 31 | 32 | 33 | ### ALERT SYNTAX ### 34 | 35 | * Symbol format:: ETHUSDT, ETHUSDT.P, ETH/USDT, ETH/USDT:USDT. All these formats will be accepted. 36 | 37 | * Account id: Just add the id you create for the account. No command associated. Account id must include at least one non-numeric character and obviously it shouldn't be the same as any of the command names. 38 | 39 | * Commands:
40 | **buy** - places buy order.
41 | **sell** - places sell order.
42 | **position or pos** - Creates a position of the given value, or modifies the pre-existing one to match it. Use a positive value for Long and a negative value for Short.
43 | **close** - closes the position (position 0 also does it).
44 | **limit:[customID]:[price]** - Combined with buy/sell commads creates a limit order. The three fields must be separated by a colon with no spaces.
45 | Every limit order must have assigned its own unique ID so it can be identified for cancelling it
46 | **cancel:[customID]** - Cancels a limit order by its customID. The symbol is required in the order.
47 | **cancel:all** - Special keyword which cancels all orders from that symbol at once.
48 | 49 | * Quantities:
50 | **[value]** - quantity in base currency. Just the number without any extra character. Base currency is the coin you're trading.
51 | **[value]$** - quantity in USDT. No command associated. Just the number and the dollar sign.
52 | **[value]@** - quantity in contracts. The value of a contract differs from exchange to exchange. Just the number and the 'at' sign.
53 | **[value]%** - quantity as percentage of total USDT balance. Use a negative value for shorts when using the position command.
54 | All quantity types are interchangeable. All can be used with buy/sell/position commands. 55 | 56 | **bclock** - When using leverage the quantity will be scaled up by the leverage by Whook. So the quantity you set in the alert will be scaled up and the cost to open the trade kept intact (the USDT cost will be always respected, the amount of coint will be always scaled). This will happen both when sending the order using base curency or quote currency (USDT). 57 | If you are using base currency values and you want them to be the final value of the order you can switch it up by adding the keyword 'bclock' to your alert message. When this keyword is present the cost will be divided by the leverage instead of scaling the quantity up
58 | 59 | Notice: Whook expects the alerts to be encoded as utf-8. Tradingview already handles this, but when sending the alerts from somewhere else you should make sure your text is encoded as utf-8 or you may run into problems with the symbols '$' and '%'. If you experience issues with orders in usdt or percentage you can alternatively add the commands 'force_usdt' or 'force_percent' to your alert to override the symbols.
60 | 61 | * Leverage:
62 | **[value]x or x[value]** - The x identifies this value as the leverage.
63 | 64 | Examples:
65 | - **Buy command using USDT:**
66 | [account_id] [symbol] [command] [value in USDT] [leverage] - **myKucoinA ETH/USDT buy 300$ x3**
67 | 68 | - **Position command using contracts:**
69 | [symbol] [command] [value in base currency] [leverage] [account_id] - **ETH/USDT position -50 x3 myKucoinA**
70 | Notice: This would open a 50ETH short at 3x. For a long position use a positive value. Same goes when the value is in USDT
71 | Example of a position alert from a strategy in Tradingview:
72 | **myKucoinA {{ticker}} pos {{strategy.position_size}} x3**
73 | This alert is all you should really need for running 90% of the strategies in TV as long as they don't make more than one order per candle. 74 | 75 | - **Sell command using base currency:**
76 | [account_id] [symbol] [command] [value in USDT] [leverage] - **myKucoinA ETH/USDT sell 0.25 x3**
77 | This would sell 0.25ETH
78 | The typical alert message to set in a Tradingview strategy for operating with buy/sell orders would look like this
79 | **myAccount {{ticker}} {{strategy.order.action}} {{strategy.order.contracts}}@ x3**
80 | If you're doing more than one order per candle you may need to use this one.
81 | 82 | - **Close position**
83 | [account_id] [symbol] [command] [percentage] - **myKucoinA ETH/USD close 33.33%**
84 | The percentage parameter is optional. If not included it will close the full position. 85 | 86 | - **Limit buy command using USDT:**
87 | [account_id] [symbol] [command] [value in USDT] [leverage] [limit:[customID]:[price]] - **myKucoinA ETH/USDT buy 300$ x3 limit:myid002:1012**
88 | Will open a buy order at 1012. The management of the customID falls on you if you ever want to cancel it. Remember you can't open 2 orders with the same customID
89 | Some exchange peculiarities to be aware of:
90 | -Bybit will not accept the same customID twice, even if the previous order is already cancelled.
91 | -Coinex only accepts numeric customIDs.
92 | 93 | - **Cancel limit order:**
94 | [account_id] [symbol] [cancel:[customID]] - **myKucoinA ETH/USDT cancel:myid002**
95 | [account_id] [symbol] [cancel:all] - **myKucoinA ETH/USDT cancel:all** all orders from this symbol
96 | 97 | 98 | Several orders can be included in the same alert, separated by line breaks. For example, you can send the orders for 2 different accounts inside the same alert. (the console will be a little messy when doing this, but the logs will be clean)
99 | 100 | It's possible to add comments inside the alert message. The comment must be in a new line and begin with a double slash '//'. Why? You ask. Because I often forget the setting I used when I created the alert! Whook will simply ignore that line when parsing the alert. 101 | 102 | 103 | ### HOW TO INSTALL AND RUN ### 104 | 105 | ##### Windows: 106 | 107 | - Download and install [python](https://www.python.org/downloads/). During the installation make sure to *enable the system PATH option* and at the end of the installation *allow it to unlimit windows PATH length*
108 | - Open the windows cmd prompt (type cmd in the windows search at the taskbar). Install the required python modules by typing "pip install ccxt" and "pip install flask" in the cmd prompt.
109 | 110 | With these you can already run the script, but it won't have access online. For giving it access to the internet I recommend to use:
111 | 112 | - [ngrok](https://ngrok.com/download). Create a free ngrok account. Download the last version of ngrok and unzip it. In the ngrok website they provide an auth key, copy it. Launch the software and paste the auth code into the ngrok console (with the authcode ngrok will be able to stay open forever). Then type in the ngrok console: "ngrok http 80". This will create an internet address for your webhook. You have to add /whook at the end of it to comunicate with the Whook server. This will be the address you introduce in the tradingview alert
113 | 114 | Note: Ngrok now allows free accounts to create one static domain. It allows to close Ngrok and keep the same address the next time you open it, which is nice. 115 | 116 | Example of an address: https://e579-139-47-50-49.ngrok-free.app/whook
117 | 118 | - You can launch the script by double clicking main.py (as long as you enabled the PATH options at installing python). If for some reason Windows failed to associate .py files with python.exe you can create a .bat file inside the same directory as main.py with this inside
119 | @echo off
120 | python.exe main.py
121 | pause
122 | 123 | 124 | ### CONFIGURATION - API KEYS ### 125 | When you first launch the script it will create an accounts.json file in the script directory and exit with a 'no accounts found' error. This file is a template to configure the accounts API data. This file can contain **as many accounts as you want separated by commas**. It looks like this: 126 | 127 | 128 | [
129 |   {
130 |    "ACCOUNT_ID":"your_account_name",
131 |    "EXCHANGE":"exchange_name",
132 |    "API_KEY":"your_api_key",
133 |    "SECRET_KEY":"your_secret_key",
134 |    "PASSWORD":"your_API_password",
135 |    "MARGIN_MODE":"isolated"
136 |   }
137 | ]
138 | 139 | 140 | You have to fill your **API_KEY** and **SECRET_KEY** information in the accounts.json file.
141 | The **ACCOUNT_ID** field is the **name you give to the account**. It's to be included in the alert message to identify the alert target account.
142 | The **PASSWORD** field is required by Kucoin and Bitget but other exchanges may or may not use it. If your exchange doesn't give you a password when creating the API key just leave the field blank.
143 | The **MARGIN_MODE** field defines the margin mode in which the account will operate. Valid names "isolated" or "cross". Defaults to isolated. It's only allowed to define it in a per account basis. There's no support to define it per symbol.
144 | The **EXCHANGE** field is self explanatory. Valid exchange names are:
145 | - "**kucoinfutures**"
146 | - "**bitget**"
147 | - "**coinex**"
148 | - "**bingx**
149 | - "**okx**"
150 | - "**okxdemo**"(for testnet)
151 | - "**bybit**"
152 | - "**bybitdemo**"(for testnet)
153 | - "**binance**"
154 | - "**binancedemo**"(for testnet)
155 | - "**krakenfutures**"
156 | - "**krakendemo**"(for testnet)
157 | - "**phemex**"
158 | - "**phemexdemo**" (for testnet)
159 | 160 | There is also one optional key: **'SETTLE_COIN'** for cases where you want to trade non-USDT pairs (or non-USD in the case of Kraken). Different settle coins can't be combined, tho. Whook will only use one at once per account. If you want to trade in several settle coins you can create an account for each settle coin (they can reuse the same API keys). 161 | 162 | 163 | ### HOW TO HOST IN AWS ### 164 | 165 | #### Windows 166 | 167 | You can host a server in AWS EC2 for free. It can be a linux server or a windows server. You can find many tutorials in Youtube on how to do it. Here's a (slightly outdated) tutorial for windows: https://youtu.be/9z5YOXhxD9Q.
168 | I host it in a Windows_server 2022 edition which was the latest at the time of writing this readme.
169 | 170 | Once you have your virtual machine running follow the steps in the section above ("How to intall and run"). 171 | 172 | #### Linux 173 | 174 | There is a Linux tutorial inside the tutorials directory. Thanks to @iZnogoude (https://github.com/iZnogoude/whook) for making it. 175 | 176 | ### KNOWN BUGS ### 177 | - Kraken: Whook is unable to set the margin mode. It will use whatever is set in the exchange for that symbol. 178 | - Kraken can't check leverage boundaries. If a order exceeds the maximum leverage the console may spam until the order times out. 179 | - BingX: Limit orders aren't setting the custom ID. They can only be cancelled using cancel:all 180 | 181 | ### TO DO LIST ### 182 | - Split whook in 2 files, one containing the accounts class. So it can be imported by other scripts to create orders directly. In short, for bots. 183 | 184 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import ccxt 4 | from flask import Flask, request, abort, jsonify 5 | from werkzeug.middleware.proxy_fix import ProxyFix 6 | from threading import Timer 7 | import os 8 | import time 9 | import os 10 | import json 11 | import copy 12 | import logging 13 | from datetime import datetime 14 | from decimal import Decimal, ROUND_CEILING, ROUND_FLOOR, ROUND_HALF_EVEN 15 | from pprint import pprint 16 | 17 | 18 | def fixVersionFormat( version )->str: 19 | vl = version.split(".") 20 | return f'{vl[0]}.{vl[1]}.{vl[2].zfill(3)}' 21 | 22 | minCCXTversion = '4.2.82' 23 | CCXTversion = fixVersionFormat(ccxt.__version__) 24 | print( 'CCXT Version:', ccxt.__version__) 25 | if( CCXTversion < fixVersionFormat(minCCXTversion) ): 26 | print( '\n============== * WARNING * ==============') 27 | print( 'WHOOK requires CCXT version', minCCXTversion,' or higher.') 28 | print( 'While it may run with earlier versions wrong behaviors are expected to happen.' ) 29 | print( 'Please update CCXT.' ) 30 | print( '============== * WARNING * ==============\n') 31 | 32 | 33 | ################### 34 | ##### Globals ##### 35 | ################### 36 | 37 | verbose = False 38 | debug_order = False 39 | SHOW_BALANCE = False # print account balance at exchange initialization 40 | SHOW_LIQUIDATION = False # in positions when available 41 | SHOW_BREAKEVEN = True # in positions when available 42 | SHOW_REALIZEDPNL = False # in position when available 43 | SHOW_ENTRYPRICE = False # in positions 44 | USE_PROXY = False 45 | PORT = 80 46 | PROXY_PORT = 50000 47 | ALERT_TIMEOUT = 60 * 3 48 | ORDER_TIMEOUT = 40 49 | REFRESH_POSITIONS_FREQUENCY = 5 * 60 # refresh positions every 5 minutes 50 | UPDATE_ORDERS_FREQUENCY = 0.25 # frametime in seconds at which the orders queue is refreshed. 51 | LOGS_DIRECTORY = 'logs' 52 | MARGIN_MODE_NONE = '------' 53 | FLOAT_ERROR = 1e-9 54 | 55 | #### Open config file ##### 56 | 57 | def writeConfig(): 58 | with open('config.json', 'w') as f: 59 | configString = '[\n\t{\n' 60 | configString += '\t\t"ALERT_TIMEOUT":'+str(ALERT_TIMEOUT)+',\n' 61 | configString += '\t\t"ORDER_TIMEOUT":'+str(ORDER_TIMEOUT)+',\n' 62 | configString += '\t\t"REFRESH_POSITIONS_FREQUENCY":'+str(REFRESH_POSITIONS_FREQUENCY)+',\n' 63 | configString += '\t\t"UPDATE_ORDERS_FREQUENCY":'+str(UPDATE_ORDERS_FREQUENCY)+',\n' 64 | configString += '\t\t"VERBOSE":'+str(verbose).lower()+',\n' 65 | configString += '\t\t"SHOW_BALANCE":'+str(SHOW_BALANCE).lower()+',\n' 66 | configString += '\t\t"SHOW_REALIZEDPNL":'+str(SHOW_REALIZEDPNL).lower()+',\n' 67 | configString += '\t\t"SHOW_ENTRYPRICE":'+str(SHOW_ENTRYPRICE).lower()+',\n' 68 | configString += '\t\t"SHOW_LIQUIDATION":'+str(SHOW_LIQUIDATION).lower()+',\n' 69 | configString += '\t\t"SHOW_BREAKEVEN":'+str(SHOW_BREAKEVEN).lower()+',\n' 70 | configString += '\t\t"LOGS_DIRECTORY":"'+str(LOGS_DIRECTORY)+'",\n' 71 | configString += '\t\t"USE_PROXY":'+str(USE_PROXY).lower()+',\n' 72 | configString += '\t\t"PROXY_PORT":'+str(PROXY_PORT)+'\n' 73 | configString += '\t}\n]' 74 | 75 | f.write( configString ) 76 | f.close() 77 | 78 | try: 79 | with open('config.json', 'r') as config_file: 80 | config = json.load(config_file) 81 | config = config[0] 82 | config_file.close() 83 | except FileNotFoundError: 84 | writeConfig() 85 | print( "Config file created.\n----------------------------") 86 | else: 87 | # parse the config file 88 | if( config.get('ALERT_TIMEOUT') != None ): 89 | ALERT_TIMEOUT = int(config.get('ALERT_TIMEOUT')) 90 | if( config.get('ORDER_TIMEOUT') != None ): 91 | ORDER_TIMEOUT = int(config.get('ORDER_TIMEOUT')) 92 | if( config.get('REFRESH_POSITIONS_FREQUENCY') != None ): 93 | REFRESH_POSITIONS_FREQUENCY = int(config.get('REFRESH_POSITIONS_FREQUENCY')) 94 | if( config.get('UPDATE_ORDERS_FREQUENCY') != None ): 95 | UPDATE_ORDERS_FREQUENCY = float(config.get('UPDATE_ORDERS_FREQUENCY')) 96 | if( config.get('SHOW_BALANCE') != None ): 97 | SHOW_BALANCE = bool(config.get('SHOW_BALANCE')) 98 | if( config.get('SHOW_REALIZEDPNL') != None ): 99 | SHOW_REALIZEDPNL = bool(config.get('SHOW_REALIZEDPNL')) 100 | if( config.get('SHOW_ENTRYPRICE') != None ): 101 | SHOW_ENTRYPRICE = bool(config.get('SHOW_ENTRYPRICE')) 102 | if( config.get('SHOW_LIQUIDATION') != None ): 103 | SHOW_LIQUIDATION = bool(config.get('SHOW_LIQUIDATION')) 104 | if( config.get('SHOW_BREAKEVEN') != None ): 105 | SHOW_BREAKEVEN = bool(config.get('SHOW_BREAKEVEN')) 106 | if( config.get('VERBOSE') != None ): 107 | verbose = bool(config.get('VERBOSE')) 108 | if( config.get('LOGS_DIRECTORY') != None ): 109 | LOGS_DIRECTORY = str(config.get('LOGS_DIRECTORY')) 110 | if( config.get('USE_PROXY') != None ): 111 | USE_PROXY = bool(config.get('USE_PROXY')) 112 | if( config.get('PROXY_PORT') != None ): 113 | PROXY_PORT = int(config.get('PROXY_PORT')) 114 | #rewrite the config file 115 | writeConfig() 116 | 117 | 118 | ##### Utils ##### 119 | 120 | def dateString(): 121 | return datetime.today().strftime("%Y/%m/%d") 122 | 123 | def timeNow(): 124 | return time.strftime("%H:%M:%S") 125 | 126 | def roundUpTick( value: float, tick: str ): 127 | if type(tick) is not str: tick = str(tick) 128 | if type(value) is not Decimal: value = Decimal( value ) 129 | return float( value.quantize( Decimal(tick), ROUND_CEILING ) ) 130 | 131 | def roundDownTick( value: float, tick: str ): 132 | if type(tick) is not str: tick = str(tick) 133 | if type(value) is not Decimal: value = Decimal( value ) 134 | return float( value.quantize( Decimal(tick), ROUND_FLOOR ) ) 135 | 136 | def roundToTick( value: float, tick: float ): 137 | if type(tick) is not str: tick = str(tick) 138 | if type(value) is not Decimal: value = Decimal( value ) 139 | return float( value.quantize( Decimal(tick), ROUND_HALF_EVEN ) ) 140 | 141 | class RepeatTimer(Timer): 142 | def run(self): 143 | while not self.finished.wait(self.interval): 144 | self.function(*self.args, **self.kwargs) 145 | 146 | class position_c: 147 | def __init__(self, symbol, position, thisMarket = None ) -> None: 148 | self.symbol = symbol 149 | self.position = position 150 | self.thisMarket = thisMarket 151 | 152 | def getKey(self, key): 153 | return self.position.get(key) 154 | 155 | def getRealizedPNL( self ): 156 | # try all the different keys from the exchanges 157 | # 158 | if( self.getKey('realizedPnl') != None ): 159 | # 'realizedPnl' # Bitget 160 | return float(self.getKey('realizedPnl')) 161 | 162 | if( self.getKey('info') != None ): 163 | info = self.getKey('info') 164 | # 'realisedPnl' # OKX, Kucoin 165 | if( info.get('realisedPnl') != None ): 166 | return float(info.get('realisedPnl')) 167 | # 'achievedProfits' # Bitget (but it has generic) 168 | if( info.get('achievedProfits') != None ): 169 | return float(info.get('achievedProfits')) 170 | # 'profit_real' # Coinex 171 | if( info.get('profit_real') != None ): 172 | return float(info.get('profit_real')) 173 | # 'cumRealisedPnl' # dirty, dirty Bybit 174 | if( info.get('cumRealisedPnl') != None ): 175 | return float(info.get('cumRealisedPnl')) 176 | 177 | return 0.0 178 | 179 | def getRealCost(self)->float: 180 | if( self.thisMarket == None ): 181 | return 0.0 182 | 183 | contracts = self.getKey('contracts') 184 | contractSize = self.thisMarket.get('contractSize') 185 | entryprice = self.getKey('entryPrice') 186 | leverage = self.thisMarket['local']['leverage'] 187 | 188 | if not contracts or not contractSize or not entryprice or leverage == 0: 189 | if self.getKey('initialMargin'): 190 | return float(self.getKey('initialMargin')) 191 | if self.getKey('collateral') : 192 | return float(self.getKey('collateral')) 193 | return 0.0 194 | 195 | return float(contractSize) * float(contracts) * float(entryprice) / leverage 196 | 197 | def generateDictionary(self)->dict: 198 | if (self.thisMarket == None): 199 | return {} 200 | 201 | # numeric values 202 | unrealizedPnl = 0.0 if (self.getKey('unrealizedPnl') == None) else float(self.getKey('unrealizedPnl')) 203 | initialMargin = 0.0 if (self.getKey('initialMargin') == None) else float(self.getKey('initialMargin')) 204 | collateral = 0.0 if (self.getKey('collateral') == None) else float(self.getKey('collateral')) 205 | 206 | if initialMargin != 0.0: 207 | pct = (unrealizedPnl / initialMargin) * 100.0 208 | elif collateral != 0.0: 209 | pct = (unrealizedPnl / (collateral - unrealizedPnl)) * 100.0 210 | else: 211 | pct = 0.0 212 | 213 | positionMode = 'hedged' if (self.thisMarket['local']['positionMode'] == 'hedged') else 'oneway' 214 | 215 | # basic fields 216 | symbol = self.symbol 217 | side = self.getKey('side') 218 | leverage = self.thisMarket['local'].get('leverage', -1.0) 219 | contracts = self.getKey('contracts') 220 | realCost = self.getRealCost() 221 | realizedPnl = self.getRealizedPNL() 222 | entryprice = self.position.get('entryPrice', -1.0) 223 | liquidationprice = self.position.get('liquidationPrice', -1.0) 224 | 225 | # break even price 226 | info = self.getKey('info') 227 | breakevenprice = None 228 | if info != None: 229 | breakevenprice = info.get('bePx') 230 | if breakevenprice == None: 231 | breakevenprice = info.get('breakEvenPrice') 232 | 233 | if breakevenprice == None: 234 | breakevenprice = -1.0 235 | 236 | result = { 237 | 'symbol': symbol, 238 | 'positionMode': positionMode, 239 | 'marginMode': self.thisMarket['local']['marginMode'], 240 | 'side': side, 241 | 'leverage': float(leverage), 242 | 'contracts': float(contracts), 243 | 'realCost': float(realCost), 244 | 'unrealizedPnl': float(unrealizedPnl), 245 | 'pct': float(pct), 246 | 'realizedPnl': float(realizedPnl), 247 | 'entryPrice': float(entryprice), 248 | 'liquidationPrice': float(liquidationprice), 249 | 'breakEvenPrice': float(breakevenprice) 250 | } 251 | 252 | return result 253 | 254 | def generatePrintString(self)->str: 255 | if( self.thisMarket == None ): 256 | return '' 257 | 258 | # Use standardized dictionary 259 | d = self.generateDictionary() 260 | if not d: 261 | return '' 262 | 263 | # small helper to format integer-like floats without .0 264 | def fmt_num(n): 265 | try: 266 | fv = float(n) 267 | except Exception: 268 | return str(n) 269 | return str(int(fv)) if fv.is_integer() else str(fv) 270 | 271 | # small formatting helpers 272 | def fmt_money(v, prec=2): 273 | try: 274 | return "{:.{p}f}[$]".format(float(v), p=prec) 275 | except Exception: 276 | return str(v) 277 | 278 | def fmt_pct(v): 279 | try: 280 | return "{:.2f}%".format(float(v)) 281 | except Exception: 282 | return str(v) 283 | 284 | def fmt_price(v): 285 | try: 286 | v = float(v) 287 | except Exception: 288 | return str(v) 289 | if v <= 0: 290 | return "----" 291 | numDecimals = max(6 - len(str(int(v))), 0) 292 | return ("{:.%df}" % numDecimals).format(v) 293 | 294 | # dynamic formatting for realCost based on magnitude 295 | def fmt_realcost_dynamic(v): 296 | try: 297 | val = float(v) 298 | except Exception: 299 | return str(v) 300 | int_part = int(abs(val)) 301 | # rules: 302 | # - if integer part is zero -> keep high precision (4 decimals) 303 | # - if integer part > 10000 -> only 1 decimal 304 | # - otherwise reduce decimals as integer digits grow 305 | if int_part == 0: 306 | prec = 4 307 | elif int_part > 10000: 308 | prec = 1 309 | else: 310 | digits = len(str(int_part)) 311 | if digits == 1: 312 | prec = 3 313 | elif digits == 2: 314 | prec = 2 315 | else: 316 | prec = 1 317 | return "{:.{p}f}[$]".format(val, p=prec) 318 | 319 | # dynamic formatting for prices (entry/liquidation/breakeven) using same magnitude rules 320 | def fmt_price_dynamic(v): 321 | try: 322 | val = float(v) 323 | except Exception: 324 | return str(v) 325 | if val <= 0: 326 | return "----" 327 | int_part = int(abs(val)) 328 | if int_part == 0: 329 | prec = 4 330 | elif int_part > 10000: 331 | prec = 1 332 | else: 333 | digits = len(str(int_part)) 334 | if digits == 1: 335 | prec = 3 336 | elif digits == 2: 337 | prec = 2 338 | else: 339 | prec = 1 340 | return "{:.{p}f}".format(val, p=prec) 341 | 342 | # prepare small strings for each field 343 | positionModeChar = '[H]' if (d.get('positionMode') == 'hedged') else '' 344 | lev = d.get('leverage', -1.0) 345 | levStr = "?x" if lev == 0 else (str(int(lev)) + 'x' if float(lev).is_integer() else str(lev) + 'x') 346 | 347 | fld_symbol = f"{d.get('symbol','').replace(':USDT', '')}{positionModeChar}" 348 | fld_margin = f"{d.get('marginMode','')}/{levStr}" 349 | fld_side = d.get('side','') 350 | fld_contracts = fmt_num(d.get('contracts', 0)) 351 | fld_realcost = fmt_realcost_dynamic(d.get('realCost', 0.0)) 352 | fld_unreal = fmt_money(d.get('unrealizedPnl', 0.0), prec=2) 353 | fld_pct = fmt_pct(d.get('pct', 0.0)) 354 | 355 | # apply the same dynamic adjustment for these fields 356 | fld_realized = ("[rp]" + fmt_realcost_dynamic(d.get('realizedPnl', 0.0))) if SHOW_REALIZEDPNL else None 357 | fld_entry = ("[ep]" + fmt_price_dynamic(d.get('entryPrice', -1.0))) if SHOW_ENTRYPRICE and d.get('entryPrice', -1.0) > 0.0 else None 358 | fld_liq = ("[li]" + fmt_price_dynamic(d.get('liquidationPrice', -1.0))) if SHOW_LIQUIDATION else None 359 | fld_be = ("[be]" + fmt_price_dynamic(d.get('breakEvenPrice', -1.0))) if SHOW_BREAKEVEN and d.get('breakEvenPrice', -1.0) > 0.0 else None 360 | 361 | # collect only non-None fields in order 362 | fields = [ 363 | fld_symbol, 364 | fld_margin, 365 | fld_side, 366 | fld_contracts, 367 | fld_realcost, 368 | fld_unreal, 369 | fld_pct, 370 | ] 371 | # optional fields appended if enabled 372 | if fld_realized: fields.append(fld_realized) 373 | if fld_entry: fields.append(fld_entry) 374 | if fld_liq: fields.append(fld_liq) 375 | if fld_be: fields.append(fld_be) 376 | 377 | # decide column widths (based on content but with sensible minimums) 378 | min_widths = [15, 12, 7, 8, 11, 12, 10, 14, 14, 14, 14] 379 | widths = [] 380 | for i, val in enumerate(fields): 381 | content_len = len(val) if val is not None else 0 382 | base = min_widths[i] if i < len(min_widths) else 10 383 | widths.append(max(base, content_len + 2)) 384 | 385 | # Determine the index where liquidation (fld_liq) appears, if present. 386 | # Starting from that column, format to the left as requested. 387 | liq_index = None 388 | if fld_liq is not None and fld_liq in fields: 389 | liq_index = fields.index(fld_liq) 390 | 391 | # build the final single-line string with columns 392 | parts = [] 393 | for i, val in enumerate(fields): 394 | w = widths[i] 395 | # left-align first three columns (symbol, margin, side) 396 | # right-align from contracts onward, except that columns starting from liquidation (if present) should be left-aligned 397 | if i < 3: 398 | parts.append(val.ljust(w)) 399 | else: 400 | if liq_index is not None and i >= liq_index: 401 | parts.append(val.ljust(w)) 402 | else: 403 | parts.append(val.rjust(w)) 404 | 405 | return " ".join(parts).rstrip() 406 | 407 | 408 | class order_c: 409 | def __init__(self, symbol = "", side = "", quantity = 0.0, leverage = 1, delay = 0, reduceOnly = False) -> None: 410 | self.symbol = symbol 411 | self.type = 'market' 412 | self.side = side 413 | self.quantity = quantity 414 | self.leverage = leverage 415 | self.price = None 416 | self.customID = None 417 | self.reduced = False 418 | self.reduceOnly = True if leverage == 0 else reduceOnly 419 | self.id = "" 420 | self.delay = delay 421 | self.timestamp = time.monotonic() 422 | def timedOut(self): 423 | return ( self.timestamp + ORDER_TIMEOUT < time.monotonic() ) 424 | def delayed(self): 425 | return (self.timestamp + self.delay > time.monotonic() ) 426 | 427 | class account_c: 428 | def __init__(self, exchange = None, name = 'default', apiKey = None, secret = None, password = None, marginMode = None, settleCoin = None )->None: 429 | 430 | self.accountName = name 431 | self.refreshPositionsFailed = 0 432 | self.positionslist = [] 433 | self.ordersQueue = [] 434 | self.activeOrders = [] 435 | self.latchedAlerts = [] 436 | self.MARGIN_MODE = 'cross' if ( marginMode != None and marginMode.lower() == 'cross') else 'isolated' 437 | self.SETTLE_COIN = 'USDT' if( settleCoin == None ) else settleCoin 438 | 439 | if( exchange == None ): 440 | raise ValueError('Exchange not defined') 441 | if( name.isnumeric() ): 442 | print( " * FATAL ERROR: Account 'id' can not be only numeric" ) 443 | raise ValueError('Invalid Account Name') 444 | if( name.lower() == 'allaccounts' ): 445 | print( " * FATAL ERROR: Account 'id' can not be 'allaccounts'" ) 446 | raise ValueError('Invalid Account Name: "allaccounts" is a reserved name.') 447 | if( name.lower() == 'allaccounts' ): 448 | print( " * FATAL ERROR: Account 'id' can not be 'allaccounts'" ) 449 | raise ValueError('Invalid Account Name: "allaccounts" is a reserved name.') 450 | 451 | if( exchange.lower() == 'kucoinfutures' ): 452 | self.exchange = ccxt.kucoinfutures( { 453 | 'apiKey': apiKey, 454 | 'secret': secret, 455 | 'password': password, 456 | 'enableRateLimit': False, 457 | "options": {'defaultType': 'swap', 'defaultMarginMode':self.MARGIN_MODE, 'adjustForTimeDifference' : True}, 458 | } ) 459 | elif( exchange.lower() == 'bitget' ): 460 | self.exchange = ccxt.bitget({ 461 | "apiKey": apiKey, 462 | "secret": secret, 463 | 'password': password, 464 | "options": {'defaultType': 'swap', 'defaultMarginMode':self.MARGIN_MODE, 'adjustForTimeDifference' : True}, 465 | #"timeout": 60000, 466 | "enableRateLimit": False 467 | }) 468 | elif( exchange.lower() == 'bingx' ): 469 | self.exchange = ccxt.bingx({ 470 | "apiKey": apiKey, 471 | "secret": secret, 472 | 'password': password, 473 | "options": {'defaultType': 'swap', 'defaultMarginMode':self.MARGIN_MODE, 'adjustForTimeDifference' : True}, 474 | #"timeout": 60000, 475 | "enableRateLimit": False 476 | }) 477 | elif( exchange.lower() == 'coinex' ): 478 | self.exchange = ccxt.coinex({ 479 | "apiKey": apiKey, 480 | "secret": secret, 481 | 'password': password, 482 | "options": {'defaultType': 'swap', 'adjustForTimeDifference' : True}, 483 | #"timeout": 60000, 484 | "enableRateLimit": False 485 | }) 486 | elif( exchange.lower() == 'phemex' ): 487 | self.exchange = ccxt.phemex({ 488 | "apiKey": apiKey, 489 | "secret": secret, 490 | 'password': password, 491 | "options": {'defaultType': 'swap', 'defaultMarginMode':self.MARGIN_MODE, 'adjustForTimeDifference' : True}, 492 | #"timeout": 60000, 493 | "enableRateLimit": False 494 | }) 495 | ###HACK!! phemex does NOT have setMarginMode when the type is SWAP 496 | self.exchange.has['setMarginMode'] = False 497 | elif( exchange.lower() == 'phemexdemo' ): 498 | self.exchange = ccxt.phemex({ 499 | "apiKey": apiKey, 500 | "secret": secret, 501 | 'password': password, 502 | "options": {'defaultType': 'swap', 'defaultMarginMode':self.MARGIN_MODE, 'adjustForTimeDifference' : True}, 503 | #"timeout": 60000, 504 | "enableRateLimit": False 505 | }) 506 | self.exchange.set_sandbox_mode( True ) 507 | ###HACK!! phemex does NOT have setMarginMode when the type is SWAP 508 | self.exchange.has['setMarginMode'] = False 509 | elif( exchange.lower() == 'bybit' ): 510 | self.exchange = ccxt.bybit({ 511 | "apiKey": apiKey, 512 | "secret": secret, 513 | 'password': password, 514 | "options": {'defaultType': 'swap', 'defaultMarginMode':self.MARGIN_MODE, 'adjustForTimeDifference' : True}, 515 | #"timeout": 60000, 516 | "enableRateLimit": True 517 | }) 518 | elif( exchange.lower() == 'bybitdemo' ): 519 | self.exchange = ccxt.bybit({ 520 | "apiKey": apiKey, 521 | "secret": secret, 522 | 'password': password, 523 | "options": {'defaultType': 'swap', 'defaultMarginMode':self.MARGIN_MODE, 'adjustForTimeDifference' : True}, 524 | #"timeout": 60000, 525 | "enableRateLimit": True 526 | }) 527 | self.exchange.set_sandbox_mode( True ) 528 | elif( exchange.lower() == 'binance' ): 529 | self.exchange = ccxt.binance({ 530 | "apiKey": apiKey, 531 | "secret": secret, 532 | 'password': password, 533 | "options": {'defaultType': 'swap', 'adjustForTimeDifference' : True}, 534 | #"timeout": 60000, 535 | "enableRateLimit": False 536 | }) 537 | elif( exchange.lower() == 'binancedemo' ): 538 | self.exchange = ccxt.binance({ 539 | "apiKey": apiKey, 540 | "secret": secret, 541 | 'password': password, 542 | "options": {'defaultType': 'swap', 'adjustForTimeDifference' : True}, 543 | #"timeout": 60000, 544 | "enableRateLimit": False 545 | }) 546 | self.exchange.set_sandbox_mode( True ) 547 | elif( exchange.lower() == 'krakenfutures' ): 548 | self.exchange = ccxt.krakenfutures({ 549 | "apiKey": apiKey, 550 | "secret": secret, 551 | 'password': password, 552 | "options": {'defaultType': 'swap', 'defaultMarginMode':self.MARGIN_MODE, 'adjustForTimeDifference' : True}, 553 | #"timeout": 60000, 554 | "enableRateLimit": True 555 | }) 556 | self.SETTLE_COIN = 'USD' 557 | if( settleCoin != None ) : self.SETTLE_COIN = settleCoin 558 | # 'options': { 'settlementCurrencies': { 'flex': ['USDT', 'BTC', 'USD', 'GBP', 'EUR', 'USDC'], 559 | elif( exchange.lower() == 'krakendemo' ): 560 | self.exchange = ccxt.krakenfutures({ 561 | "apiKey": apiKey, 562 | "secret": secret, 563 | 'password': password, 564 | "options": {'defaultType': 'swap', 'defaultMarginMode':self.MARGIN_MODE, 'adjustForTimeDifference' : True}, 565 | #"timeout": 60000, 566 | "enableRateLimit": True 567 | }) 568 | self.exchange.set_sandbox_mode( True ) 569 | self.SETTLE_COIN = 'USD' 570 | if( settleCoin != None ) : self.SETTLE_COIN = settleCoin 571 | # 'options': { 'settlementCurrencies': { 'flex': ['USDT', 'BTC', 'USD', 'GBP', 'EUR', 'USDC'], 572 | elif( exchange.lower() == 'okx' ): 573 | self.exchange = ccxt.okx ({ 574 | "apiKey": apiKey, 575 | "secret": secret, 576 | 'password': password, 577 | "options": {'defaultType': 'swap', 'adjustForTimeDifference' : True}, 578 | #"timeout": 60000, 579 | "enableRateLimit": True 580 | }) 581 | elif( exchange.lower() == 'okxdemo' ): 582 | self.exchange = ccxt.okx ({ 583 | "apiKey": apiKey, 584 | "secret": secret, 585 | 'password': password, 586 | "options": {'defaultType': 'swap', 'adjustForTimeDifference' : True}, 587 | #"timeout": 60000, 588 | "enableRateLimit": True 589 | }) 590 | self.exchange.set_sandbox_mode( True ) 591 | else: 592 | raise ValueError('Unsupported exchange') 593 | 594 | if( self.exchange == None ): 595 | raise ValueError('Exchange creation failed') 596 | 597 | # crate a logger for each account 598 | 599 | # make sure the logs directory exists 600 | if( LOGS_DIRECTORY == '' ): 601 | path = f'{self.accountName}.log' 602 | else: 603 | path = f'{LOGS_DIRECTORY}/{self.accountName}.log' 604 | script_dir = os.path.dirname(os.path.realpath(__file__)) 605 | if not os.path.exists(os.path.join(script_dir, LOGS_DIRECTORY)): 606 | os.makedirs(os.path.join(script_dir, LOGS_DIRECTORY)) 607 | 608 | 609 | self.logger = logging.getLogger( self.accountName ) 610 | fh = logging.FileHandler( path ) 611 | self.logger.addHandler( fh ) 612 | self.logger.level = logging.INFO 613 | 614 | # Some exchanges don't have all fields properly filled, but we can find out 615 | # the values in another field. Instead of adding exceptions at each other function 616 | # let's reconstruct the markets dictionary trying to fix those values 617 | self.markets = {} 618 | markets = self.exchange.load_markets() 619 | marketKeys = markets.keys() 620 | for key in marketKeys: 621 | thisMarket = markets[key] 622 | if( thisMarket.get('settle') != self.SETTLE_COIN ): # double check 623 | continue 624 | 625 | if( thisMarket.get('contractSize') == None ): 626 | # in Phemex we can extract the contractSize from the description. 627 | # it's always going to be 1, but let's handle it in case they change it 628 | if( self.exchange.id == 'phemex' ): 629 | description = thisMarket['info'].get('description') 630 | s = description[ description.find('Each contract is worth') + len('Each contract is worth ') : ] 631 | list = s.split( ' ', 1 ) 632 | cs = float( list[0] ) 633 | if( cs != 1.0 ): 634 | print( "* WARNING: phemex", key, "contractSize reported", cs ) 635 | thisMarket['contractSize'] = cs 636 | else: 637 | print( "WARNING: Market", self.exchange.id, "doesn't have contractSize" ) 638 | 639 | # make sure the market has a precision value 640 | try: 641 | precision = thisMarket['precision'].get('amount') 642 | except Exception as e: 643 | raise ValueError( "Market", self.exchange.id, "doesn't have precision value" ) 644 | 645 | # some exchanges don't have a minimum purchase amount defined 646 | try: 647 | minAmount = thisMarket['limits']['amount'].get('min') 648 | except Exception as e: 649 | minAmount = None 650 | l = thisMarket.get('limits') 651 | if( l != None ): 652 | a = l.get('amount') 653 | if( a != None ): 654 | minAmount = a.get('min') 655 | 656 | if( minAmount == None ): # replace minimum amount with precision value 657 | thisMarket['limits']['amount']['min'] = float(precision) 658 | 659 | 660 | # HACK: Bingx has wrong leverage limits defined 661 | if( self.exchange.id == 'bingx' ): 662 | thisMarket['limits']['leverage']['max'] = None if self.exchange.has['fetchLeverage'] else max( 100, thisMarket['limits']['leverage']['max'] ) 663 | 664 | # also generate a local list to keep track of marginMode and Leverage status 665 | thisMarket['local'] = { 'marginMode':MARGIN_MODE_NONE, 'leverage':0, 'positionMode':'' } 666 | if( self.exchange.has.get('setPositionMode') != True ): 667 | thisMarket['local']['positionMode'] = 'oneway' 668 | 669 | # Store the market into the local markets dictionary 670 | self.markets[key] = thisMarket 671 | 672 | if( verbose ): 673 | pprint( self.markets['BTC/' + self.SETTLE_COIN + ':' + self.SETTLE_COIN] ) 674 | 675 | self.refreshPositions(True) 676 | 677 | 678 | 679 | ## methods ## 680 | 681 | def print( self, *args, sep=" ", **kwargs ): # adds account and exchange information to the message 682 | print( timeNow(), '['+ self.accountName +'/'+ self.exchange.id +'] '+ sep.join(map(str,args)), **kwargs ) 683 | self.logger.info( '['+ dateString()+']['+timeNow()+'] ' +sep.join(map(str,args)), **kwargs) 684 | 685 | def verifyLeverageRange( self, symbol, leverage )->int: 686 | 687 | leverage = max( leverage, 1 ) 688 | maxLeverage = self.findMaxLeverageForSymbol( symbol ) 689 | 690 | if( maxLeverage != None and maxLeverage < leverage ): 691 | self.print( " * WARNING: Leverage out of bounds. Readjusting to", str(maxLeverage)+"x" ) 692 | leverage = maxLeverage 693 | 694 | return leverage 695 | 696 | 697 | def updateSymbolPositionMode( self, symbol ): 698 | 699 | # Make sure the exchange is in oneway mode 700 | 701 | if( self.exchange.has.get('setPositionMode') != True and self.markets[ symbol ]['local']['positionMode'] != 'oneway' ): 702 | self.print( " * E: updateSymbolPositionMode: Exchange", self.exchange.id, "doesn't have setPositionMode nor is set to oneway" ) 703 | return 704 | 705 | if( self.markets[ symbol ]['local']['positionMode'] != 'oneway' and self.exchange.has.get('setPositionMode') == True ): 706 | if( self.getPositionBySymbol(symbol) != None ): 707 | self.print( ' * W: Cannot change position mode while a position is open' ) 708 | return 709 | 710 | try: 711 | response = self.exchange.set_position_mode( False, symbol ) 712 | except ccxt.NoChange as e: 713 | self.markets[ symbol ]['local']['positionMode'] = 'oneway' 714 | except Exception as e: 715 | for a in e.args: 716 | if( '"retCode":140025' in a or '"code":-4059' in a 717 | or 'retCode":110025' in a or '"code":"59000"' in a ): 718 | # this is not an error, but just an acknowledge 719 | # bybit {"retCode":140025,"retMsg":"position mode not modified","result":{},"retExtInfo":{},"time":1690530385019} 720 | # bybit {"retCode":110025,"retMsg":"Position mode is not modified","result":{},"retExtInfo":{},"time":1694988241696} 721 | # binance {"code":-4059,"msg":"No need to change position side."} 722 | # okx {"code":"59000","data":[],"msg":"Setting failed. Cancel any open orders, close positions, and stop trading bots first."} 723 | self.markets[ symbol ]['local']['positionMode'] = 'oneway' 724 | else: 725 | print( " * E: updateSymbolLeverage->set_position_mode:", a, type(e) ) 726 | else: 727 | # was everything correct, tho? 728 | code = 0 729 | if( self.exchange.id == 'bybit' ): # they didn't receive enough love as children 730 | code = int(response.get('retCode')) 731 | else: 732 | code = int(response.get('code')) 733 | # 'code': '0' <- coinex 734 | # 'code': '00000' <- bitget 735 | # 'code': '0' <- phemex 736 | # 'retCode': '0' <- bybit 737 | # {'code': '200', 'msg': 'success'} <- binance 738 | if( self.exchange.id == 'binance' and code == 200 or code == -4059 ): 739 | code = 0 740 | 741 | if( code != 0 ): 742 | print( " * E: updateSymbolLeverage->set_position_mode:", response ) 743 | return 744 | 745 | self.markets[ symbol ]['local']['positionMode'] = 'oneway' 746 | 747 | 748 | def updateSymbolLeverage( self, symbol, leverage ): 749 | # also sets marginMode 750 | 751 | if( leverage < 1 ): # leverage 0 indicates we are closing a position 752 | return 753 | 754 | # Notice: Kucoin is never going to make any of these. 755 | 756 | # Coinex doesn't accept any number as leverage. It must be on the list. Also clamp to max allowed 757 | leverage = self.verifyLeverageRange( symbol, leverage ) 758 | 759 | ########################################## 760 | # Update marginMode if needed 761 | ########################################## 762 | if( self.markets[ symbol ]['local']['marginMode'] != self.MARGIN_MODE and self.exchange.has.get('setMarginMode') == True ): 763 | 764 | params = {} 765 | # coinex and bybit expect the leverage as part of the marginMode call 766 | if( self.exchange.id == 'coinex' or self.exchange.id == 'bybit' ): 767 | params['leverage'] = leverage 768 | elif( self.exchange.id == 'okx' ): 769 | params['lever'] = leverage 770 | 771 | try: 772 | response = self.exchange.set_margin_mode( self.MARGIN_MODE, symbol, params ) 773 | 774 | except ccxt.NoChange as e: 775 | self.markets[ symbol ]['local']['marginMode'] = self.MARGIN_MODE 776 | except ccxt.MarginModeAlreadySet as e: 777 | self.markets[ symbol ]['local']['marginMode'] = self.MARGIN_MODE 778 | except Exception as e: 779 | for a in e.args: 780 | if( '"retCode":140026' in a or "No need to change margin type" in a 781 | or '"retCode":110026' in a ): 782 | # bybit throws an exception just to inform us the order wasn't neccesary (doh) 783 | # bybit {"retCode":140026,"retMsg":"Isolated not modified","result":{},"retExtInfo":{},"time":1690530385642} 784 | # bybit setMarginMode() marginMode must be either ISOLATED_MARGIN or REGULAR_MARGIN or PORTFOLIO_MARGIN 785 | # bybit {"retCode":110026,"retMsg":"Cross/isolated margin mode is not modified","result":{},"retExtInfo":{},"time":1695526888984} 786 | # binance {'code': -4046, 'msg': 'No need to change margin type.'} 787 | # updateSymbolLeverage->set_margin_mode: {'code': -4046, 'msg': 'No need to change margin type.'} 788 | self.markets[ symbol ]['local']['marginMode'] = self.MARGIN_MODE 789 | if( self.exchange.id == 'bitget' and 'code":"45117' in a): 790 | print( " * W: Bitget: Currently holding positions or orders, the margin mode cannot be adjusted" ) 791 | #self.markets[ symbol ]['local']['marginMode'] = 'cross' if self.MARGIN_MODE == 'isolated' else 'isolated' 792 | # * E: updateSymbolLeverage->set_margin_mode: bitget {"code":"45117","msg":"Currently holding positions or orders, the margin mode cannot be adjusted","requestTime":1734896200804,"data":null} 793 | # * E: UpdateOrdersQueue: Unhandled exception. Cancelling: bitget {"code":"45117","msg":"Currently holding positions or orders, the margin mode cannot be adjusted","requestTime":1734896201207,"data":null} 794 | else: 795 | print( " * E: updateSymbolLeverage->set_margin_mode:", a, type(e) ) 796 | else: 797 | 798 | # was everything correct, tho? 799 | code = 0 800 | if( self.exchange.id == 'bybit' ): # they didn't receive enough love as children 801 | code = int(response.get('retCode')) 802 | else: 803 | code = int(response.get('code')) 804 | # 'code': '0' <- coinex 805 | # 'code': '00000' <- bitget 806 | # 'code': '0' <- phemex 807 | # 'retCode': '0' <- bybit 808 | # {'code': '200', 'msg': 'success'} <- binance 809 | if( self.exchange.id == 'binance' and code == 200 or code == -4046 ): 810 | code = 0 811 | 812 | if( code != 0 ): 813 | print( " * E: updateSymbolLeverage->set_margin_mode:", response ) 814 | else: 815 | self.markets[ symbol ]['local']['marginMode'] = self.MARGIN_MODE 816 | 817 | # coinex and bybit don't need to continue since they have already updated the leverage 818 | if( self.exchange.id == 'coinex' or self.exchange.id == 'bybit' ): 819 | self.markets[ symbol ]['local']['leverage'] = leverage 820 | return 821 | 822 | ########################################## 823 | # Finally update leverage 824 | ########################################## 825 | if( self.markets[ symbol ]['local']['leverage'] != leverage and self.exchange.has.get('setLeverage') == True ): 826 | 827 | # from phemex API documentation: The sign of leverageEr indicates margin mode, 828 | # i.e. leverage <= 0 means cross-margin-mode, leverage > 0 means isolated-margin-mode. 829 | 830 | params = {} 831 | if( self.exchange.id == 'coinex' ): # coinex always updates leverage and marginMode at the same time 832 | params['marginMode'] = self.markets[ symbol ]['local']['marginMode'] # use current marginMode to avoid triggering an error 833 | elif( self.exchange.id == 'okx' ): 834 | params['marginMode'] = self.markets[ symbol ]['local']['marginMode'] 835 | params['posSide'] = 'net' 836 | elif( self.exchange.id == 'bingx' ): 837 | if( self.markets[ symbol ]['local']['positionMode'] != 'oneway' ): 838 | response = self.exchange.set_leverage( leverage, symbol, params = {'side':'LONG'} ) 839 | response2 = self.exchange.set_leverage( leverage, symbol, params = {'side':'SHORT'} ) 840 | if( response.get('code') == '0' and response2.get('code') == '0' ): 841 | self.markets[ symbol ]['local']['leverage'] = leverage 842 | return 843 | else: 844 | params['side'] = 'BOTH' 845 | 846 | try: 847 | response = self.exchange.set_leverage( leverage, symbol, params ) 848 | except ccxt.NoChange as e: 849 | self.markets[ symbol ]['local']['leverage'] = leverage 850 | except Exception as e: 851 | for a in e.args: 852 | if( '"retCode":140043' in a or '"retCode":110043' in a ): 853 | # bybit throws an exception just to inform us the order wasn't neccesary (doh) 854 | # bybit {"retCode":110043,"retMsg":"Set leverage not modified","result":{},"retExtInfo":{},"time":1694988242174} 855 | # bybit {"retCode":140043,"retMsg":"leverage not modified","result":{},"retExtInfo":{},"time":1690530386264} 856 | pass 857 | elif( 'MAX_LEVERAGE_OUT_OF_BOUNDS' in a ): 858 | self.print( " * E: Maximum leverage exceeded [", leverage, "]" ) 859 | return 860 | # {"status":"INTERNAL_SERVER_ERROR","result":"error","errors":[{"code":98,"message":"MAX_LEVERAGE_OUT_OF_BOUNDS"}],"serverTime":"2023-09-24T00:57:08.908Z"} 861 | else: 862 | print( " * E: updateSymbolLeverage->set_leverage:", a, type(e) ) 863 | else: 864 | # was everything correct, tho? 865 | code = 0 866 | if( self.exchange.id == 'bybit' ): # they didn't receive enough love as children 867 | code = int(response.get('retCode')) 868 | elif( self.exchange.id == 'krakenfutures' ): 869 | #{'result': 'success', 'serverTime': '2023-09-22T21:25:47.729Z'} 870 | # Error: updateSymbolLeverage->set_leverage: {'result': 'success', 'serverTime': '2023-09-22T21:30:17.767Z'} 871 | if( 'success' not in response ): 872 | code = -1 if response.get('result') != 'success' else 0 873 | elif( self.exchange.id != 'binance' ): 874 | code = int(response.get('code')) 875 | # 'code': '0' <- coinex 876 | # 'code': '00000' <- bitget 877 | # 'code': '0' <- phemex 878 | # 'retCode': '0' <- bybit 879 | # binance doesn't send any code #{'symbol': 'BTCUSDT', 'leverage': '7', 'maxNotionalValue': '40000000'} 880 | if( code != 0 ): 881 | print( " * E: updateSymbolLeverage->set_leverage:", response ) 882 | else: 883 | self.markets[ symbol ]['local']['leverage'] = leverage 884 | 885 | 886 | 887 | def fetchBalance(self): 888 | params = { "settle":self.SETTLE_COIN } 889 | if( self.exchange.id == 'krakenfutures' ): 890 | params['type'] = 'flex' 891 | 892 | response = self.exchange.fetch_balance( params ) 893 | 894 | if( self.exchange.id == 'krakenfutures' ): 895 | data = response['info']['accounts']['flex'] 896 | return { 'free':float(data.get('availableMargin')), 'used':float(data.get('initialMarginWithOrders')), 'total': float(data.get('balanceValue')) } 897 | 898 | if( response.get(self.SETTLE_COIN) == None ): 899 | balance = { 'free':0.0, 'used':0.0, 'total':0.0 } 900 | return balance 901 | 902 | return response.get(self.SETTLE_COIN) 903 | 904 | 905 | def fetchAvailableBalance(self)->float: 906 | return float( self.fetchBalance().get( 'free' ) ) 907 | 908 | 909 | def fetchBuyPrice(self, symbol)->float: 910 | orderbook = self.exchange.fetch_order_book(symbol) 911 | ask = orderbook['asks'][0][0] if len (orderbook['asks']) > 0 else None 912 | if( ask == None ): 913 | raise ValueError( "Couldn't fetch ask price" ) 914 | return ask 915 | 916 | 917 | def fetchSellPrice(self, symbol)->float: 918 | orderbook = self.exchange.fetch_order_book(symbol) 919 | bid = orderbook['bids'][0][0] if len (orderbook['bids']) > 0 else None 920 | if( bid == None ): 921 | raise ValueError( "Couldn't fetch bid price" ) 922 | return bid 923 | 924 | 925 | def fetchAveragePrice(self, symbol)->float: 926 | orderbook = self.exchange.fetch_order_book(symbol) 927 | bid = orderbook['bids'][0][0] if len (orderbook['bids']) > 0 else None 928 | ask = orderbook['asks'][0][0] if len (orderbook['asks']) > 0 else None 929 | if( bid == None and ask == None ): 930 | raise ValueError( "Couldn't fetch orderbook" ) 931 | if( bid == None ): bid = ask 932 | if( ask == None ): ask = bid 933 | return ( bid + ask ) * 0.5 934 | 935 | 936 | def getPositionBySymbol(self, symbol)->position_c: 937 | for pos in self.positionslist: 938 | if( pos.symbol == symbol ): 939 | return pos 940 | return None 941 | 942 | 943 | def findSymbolFromPairName(self, pairString): 944 | # this is only for the pair name we receive in the alert. 945 | # Once it's converted to ccxt symbol format there is no 946 | # need to use this method again. 947 | 948 | paircmd = pairString.upper() 949 | 950 | if( paircmd.endswith('.P' ) ): 951 | paircmd = paircmd[:-2] 952 | 953 | # first let's check if the pair string contains 954 | # a backslash. If it does it's probably already a symbol 955 | if '/' not in paircmd and paircmd.endswith(self.SETTLE_COIN): 956 | paircmd = paircmd[:-len(self.SETTLE_COIN)] 957 | paircmd += '/' + self.SETTLE_COIN + ':' + self.SETTLE_COIN 958 | 959 | # but it also may not include the ':USDT' ending 960 | if '/' in paircmd and not paircmd.endswith(':'+ self.SETTLE_COIN ): 961 | paircmd += ':' + self.SETTLE_COIN 962 | 963 | # try the more direct approach 964 | m = self.markets.get(paircmd) 965 | if( m != None ): 966 | return m.get('symbol') 967 | 968 | # so now let's find it in the list using the id 969 | for m in self.markets: 970 | id = self.markets[m]['id'] 971 | symbol = self.markets[m]['symbol'] 972 | if( symbol == paircmd or id == paircmd ): 973 | return symbol 974 | return None 975 | 976 | 977 | def findContractSizeForSymbol(self, symbol)->float: 978 | return self.markets[symbol].get('contractSize') 979 | 980 | 981 | def findPrecisionForSymbol(self, symbol)->float: 982 | if( self.exchange.id == 'bingx' ): 983 | precision = 1.0 / (10.0 ** self.markets[symbol]['precision'].get('amount')) 984 | else : 985 | precision = self.markets[symbol]['precision'].get('amount') 986 | return precision 987 | 988 | 989 | def findMinimumAmountForSymbol(self, symbol)->float: 990 | return self.markets[symbol]['limits']['amount'].get('min') 991 | 992 | 993 | def findMaxLeverageForSymbol(self, symbol)->float: 994 | maxLeverage = self.markets[symbol]['limits']['leverage'].get('max') 995 | if( maxLeverage == None ): 996 | maxLeverage = 100 997 | if( self.exchange.has['fetchLeverage'] ): 998 | info = self.exchange.fetch_leverage( symbol ).get('info') 999 | 1000 | if( info != None and info.get('maxLongLeverage') != None and info.get('maxShortLeverage') != None ): 1001 | maxLeverage = min(int(info['maxLongLeverage']), int(info['maxShortLeverage'])) 1002 | 1003 | self.markets[symbol]['limits']['leverage']['max'] = maxLeverage 1004 | 1005 | return maxLeverage 1006 | 1007 | 1008 | def contractsFromUSDT(self, symbol, amount, price, leverage = 1.0 )->float : 1009 | contractSize = self.findContractSizeForSymbol( symbol ) 1010 | coin = Decimal( (amount * leverage) / (contractSize * price) ) 1011 | precision = str(self.findPrecisionForSymbol( symbol )) 1012 | 1013 | return roundDownTick( coin, precision ) if ( coin > 0 ) else roundUpTick( coin, precision ) 1014 | 1015 | 1016 | def refreshPositions(self, v = verbose): 1017 | ### https://docs.ccxt.com/#/?id=position-structure ### 1018 | failed = False 1019 | try: 1020 | symbols = None 1021 | if( self.exchange.id == 'bitget' ): 1022 | symbols = list(self.markets.keys()) 1023 | positions = self.exchange.fetch_positions( symbols, params = {'settle':self.SETTLE_COIN} ) # the 'settle' param is only required by phemex 1024 | 1025 | except Exception as e: 1026 | a = e.args[0] 1027 | if 'OK' in a: # Coinex raises an exception to give an OK message when there are no positions... don't look at me, look at them 1028 | positions = [] 1029 | elif( isinstance(e, ccxt.OnMaintenance) or isinstance(e, ccxt.NetworkError) 1030 | or isinstance(e, ccxt.RateLimitExceeded) or isinstance(e, ccxt.RequestTimeout) 1031 | or isinstance(e, ccxt.ExchangeNotAvailable) or isinstance(e, ccxt.ExchangeError) or 'not available' in a ): 1032 | failed = True 1033 | 1034 | if( 'Remote end closed connection' in a 1035 | or '500 Internal Server Error' in a 1036 | or 'Internal Server Error' in a 1037 | or 'Server busy' in a or 'System busy' in a 1038 | or '"retCode":10002' in a ): 1039 | print( timeNow(), self.exchange.id, '* E: Refreshpositions:(old)', a, type(e) ) 1040 | 1041 | elif 'code":-2015' in a: # For some reason 'binancedemo' makes it all the way here without a valid API key. 1042 | print( timeNow(), a ) 1043 | return 1044 | elif 'access_id not exists': # and now coinex is doing it too. IDK why they reach here. They didn't before. 1045 | print( timeNow(), a ) 1046 | return 1047 | 1048 | 1049 | elif( 'Remote end closed connection' in a 1050 | or '500 Internal Server Error' in a 1051 | or 'Internal Server Error' in a 1052 | or 'Server busy' in a or 'System busy' in a 1053 | or 'not available' in a # ccxt.base.errors.ExchangeError 1054 | or 'failure to get a peer' in a # ccxt.base.errors.ExchangeError (okx) 1055 | or '"code":39999' in a 1056 | or 'internal error' in a # ccxt.base.errors.ExchangeError (coinex) 1057 | or '"retCode":10002' in a ): 1058 | failed = True 1059 | # this print is temporary to try to replace the string with the error type if possible 1060 | print( timeNow(), self.exchange.id, '* E: Refreshpositions:', a, type(e) ) 1061 | else: 1062 | print( timeNow(), self.exchange.id, '* E: Refreshpositions:', a, type(e) ) 1063 | failed = True 1064 | 1065 | if( failed ): 1066 | self.refreshPositionsFailed += 1 1067 | if( self.refreshPositionsFailed == 10 ): 1068 | print( timeNow(), self.exchange.id, '* W: Refreshpositions has failed 10 times in a row' ) 1069 | return 1070 | 1071 | if (self.refreshPositionsFailed >= 10 ): 1072 | print( timeNow(), self.exchange.id, '* W: Refreshpositions has returned to activity' ) 1073 | 1074 | self.refreshPositionsFailed = 0 1075 | 1076 | # Phemex returns positions that were already closed 1077 | # reconstruct the list of positions only with active positions 1078 | cleanPositionsList = [] 1079 | for thisPosition in positions: 1080 | if( abs(thisPosition.get('contracts', 0.0)) < FLOAT_ERROR ): 1081 | continue 1082 | cleanPositionsList.append( thisPosition ) 1083 | positions = cleanPositionsList 1084 | 1085 | numPositions = len(positions) 1086 | 1087 | if v: 1088 | tab = ' ' 1089 | if( numPositions > 0 ) : print('------------------------------') 1090 | # fetch balance 1091 | balanceString = '' 1092 | if SHOW_BALANCE: 1093 | balance = self.fetchBalance() 1094 | balanceString = " Balance: {:.2f}[$]".format(balance['total']) 1095 | balanceString += " - Available {:.2f}[$]".format(balance['free']) 1096 | print( tab + str(numPositions), "positions found.", balanceString ) 1097 | 1098 | self.positionslist.clear() 1099 | for thisPosition in positions: 1100 | 1101 | symbol = thisPosition.get('symbol') 1102 | 1103 | # HACK!! bybit response doesn't contain a 'hedge' key, but it contains the information in the 'info' block 1104 | if( self.exchange.id == 'bybit' ): 1105 | thisPosition['hedged'] = True if( thisPosition['info'].get( 'positionIdx' ) != '0' ) else False 1106 | 1107 | if( self.exchange.id == 'bingx' ): # 'onlyOnePosition': True, 1108 | thisPosition['hedged'] = not thisPosition['info'].get( 'onlyOnePosition' ) 1109 | 1110 | # if the position contains positionMode information update our local data 1111 | if( thisPosition.get('hedged') != None ) : # None means the exchange only supports oneWay 1112 | self.markets[ symbol ]['local'][ 'positionMode' ] = 'hedged' if( thisPosition.get('hedged') == True ) else 'oneway' 1113 | 1114 | 1115 | # if the position contains the marginMode information also update the local data 1116 | 1117 | #some exchanges have the key set to None. Fix it when possible 1118 | if( thisPosition.get('marginMode') == None ) : 1119 | if( self.exchange.id == 'bybit' ): # tradeMode - Classic & UTA (inverse): 0: cross-margin, 1: isolated margin 1120 | self.markets[ symbol ]['local'][ 'marginMode' ] = 'isolated' if thisPosition['info']['tradeMode'] == '1' else 'cross' 1121 | elif( self.exchange.has.get('setMarginMode') != True ): 1122 | thisPosition['marginMode'] = MARGIN_MODE_NONE 1123 | else: 1124 | print( ' * W: refreshPositions: Could not get marginMode for', symbol ) 1125 | thisPosition['marginMode'] = MARGIN_MODE_NONE 1126 | else: 1127 | self.markets[ symbol ]['local'][ 'marginMode' ] = thisPosition.get('marginMode') 1128 | 1129 | # update the local leverage as well as we can 1130 | leverage = -1 1131 | if( thisPosition.get('leverage') != None ): 1132 | leverage = int(thisPosition.get('leverage')) 1133 | if( leverage != thisPosition.get('leverage') ): # kucoin sends weird fractional leverage. Ignore it 1134 | leverage = -1 1135 | 1136 | # still didn't find the leverage, but the exchange has the fetchLeverage method so we can try that. 1137 | if( leverage == -1 and self.exchange.has.get('fetchLeverage') == True ): 1138 | try: 1139 | response = self.exchange.fetch_leverage( symbol ) 1140 | except Exception as e: 1141 | pass 1142 | else: 1143 | if( self.exchange.id == 'bitget' ): 1144 | if( response['data']['marginMode'] == 'crossed' ): 1145 | leverage = int(response['data'].get('crossMarginLeverage')) 1146 | else: 1147 | # they should always be the same 1148 | longLeverage = int(response['data'].get('fixedLongLeverage')) 1149 | shortLeverage = int(response['data'].get('fixedShortLeverage')) 1150 | if( longLeverage == shortLeverage ): 1151 | leverage = longLeverage 1152 | 1153 | elif( self.exchange.id == 'bingx' ): 1154 | # they should always be the same 1155 | longLeverage = response['data'].get('longLeverage') 1156 | shortLeverage = response['data'].get('shortLeverage') 1157 | if( longLeverage == shortLeverage ): 1158 | leverage = longLeverage 1159 | 1160 | if( leverage != -1 ): 1161 | self.markets[ symbol ]['local'][ 'leverage' ] = leverage 1162 | elif( self.exchange.id != "kucoinfutures" and self.exchange.id != "binance" ): # we know kucoin is helpless. And apparently Binance. 1163 | print( " * W: refreshPositions: Couldn't find leverage for", self.exchange.id ) 1164 | 1165 | self.positionslist.append(position_c( symbol, thisPosition, self.markets[ symbol ] )) 1166 | 1167 | if v: 1168 | for pos in self.positionslist: 1169 | print( tab + pos.generatePrintString() ) 1170 | 1171 | print('------------------------------') 1172 | 1173 | 1174 | def activeOrderForSymbol(self, symbol ): 1175 | for o in self.activeOrders: 1176 | if( o.symbol == symbol ): 1177 | return True 1178 | return False 1179 | 1180 | 1181 | def fetchClosedOrderById(self, symbol, id ): 1182 | try: 1183 | response = self.exchange.fetch_closed_orders( symbol, params = {'settleCoin':self.SETTLE_COIN} ) 1184 | except Exception as e: 1185 | #Exception: ccxt.base.errors.ExchangeError: phemex {"code":39999,"msg":"Please try again.","data":null} 1186 | return None 1187 | 1188 | for o in response: 1189 | if o.get('id') == id : 1190 | return o 1191 | if verbose : print( "r...", end = '' ) 1192 | return None 1193 | 1194 | 1195 | def fetchOpenOrderById(self, symbol, id ): 1196 | try: 1197 | response = self.exchange.fetch_open_orders( symbol, params = {'settleCoin':self.SETTLE_COIN} ) 1198 | except Exception as e: 1199 | #Exception: ccxt.base.errors.ExchangeError: phemex {"code":39999,"msg":"Please try again.","data":null} 1200 | return None 1201 | 1202 | for o in response: 1203 | if o.get('id') == id : 1204 | return o 1205 | if verbose : print( "r...", end = '' ) 1206 | return None 1207 | 1208 | 1209 | def removeFirstCompletedOrder(self): 1210 | # go through the queue and remove the first completed order 1211 | for order in self.activeOrders: 1212 | if( order.timedOut() ): 1213 | self.print( " * E: Active Order Timed out", order.symbol, order.side, order.quantity, str(order.leverage)+'x' ) 1214 | self.activeOrders.remove( order ) 1215 | continue 1216 | 1217 | # Phemex doesn't support fetch_order (by id) in swap mode, but it supports fetch_open_orders and fetch_closed_orders 1218 | if( self.exchange.id == 'phemex' or self.exchange.id == 'bybit' or self.exchange.id == 'krakenfutures' ): 1219 | if( order.type == 'limit' ): 1220 | response = self.fetchOpenOrderById( order.symbol, order.id ) 1221 | else: 1222 | response = self.fetchClosedOrderById( order.symbol, order.id ) 1223 | if( response == None ): 1224 | continue 1225 | else: 1226 | try: 1227 | response = self.exchange.fetch_order( order.id, order.symbol ) 1228 | except Exception as e: 1229 | if( isinstance(e, ccxt.InvalidOrder) or 'order not exists' in e.args[0] ): 1230 | continue 1231 | 1232 | self.print( " * E: removeFirstCompletedOrder:", e, type(e) ) 1233 | continue 1234 | 1235 | 1236 | if( response == None ): # FIXME: Check if this is really happening by printing it. 1237 | print( ' * E: removeFirstCompletedOrder: fetch_order returned None' ) 1238 | continue 1239 | if( len(response) == 0 ): 1240 | print( ' * E: removeFirstCompletedOrder: fetch_order returned empty' ) 1241 | continue 1242 | 1243 | status = response.get('status') 1244 | remaining = float( response.get('remaining') ) 1245 | price = response.get('price') 1246 | if verbose : pprint( response ) 1247 | 1248 | if( order.type == 'limit' ): 1249 | if( self.exchange.id == 'coinex' ) : response['clientOrderId'] = response['info']['client_id'] #HACK!! 1250 | self.print( " * Linmit order placed:", order.symbol, order.side, order.quantity, str(order.leverage)+"x", "at price", price, 'id', response.get('clientOrderId') ) 1251 | self.activeOrders.remove( order ) 1252 | return True 1253 | 1254 | if( remaining > 0 and (status == 'canceled' or status == 'closed') ): 1255 | print("r...", end = '') 1256 | self.ordersQueue.append( order_c( order.symbol, order.side, remaining, order.leverage, 0.5 ) ) 1257 | self.activeOrders.remove( order ) 1258 | return True 1259 | 1260 | if ( status == 'closed' or status == 'filled' ): 1261 | self.print( " * Order successful:", order.symbol, order.side, order.quantity, str(order.leverage)+"x", "at price", price, 'id', order.id ) 1262 | self.activeOrders.remove( order ) 1263 | return True 1264 | return False 1265 | 1266 | 1267 | def cancelLimitOrder(self, symbol, customID )->bool: 1268 | id = customID 1269 | params = {} 1270 | 1271 | if( self.exchange.id == 'krakenfutures' or self.exchange.id == 'kucoinfutures' or self.exchange.id == 'coinex' or self.exchange.id == 'bitget' ): 1272 | # uuuggggghhhh. why do you do this to me 1273 | try: 1274 | response = self.exchange.fetch_open_orders( symbol, params = {'settleCoin':self.SETTLE_COIN} ) 1275 | except Exception as e: 1276 | self.print( ' * E: Unhandled exception in cancelLimitOrder:', e.args[0], type(e) ) 1277 | return 1278 | else: 1279 | for o in response: 1280 | if( ( o['info'].get('cliOrdId') != None and o['info']['cliOrdId'] == customID ) 1281 | or ( o['info'].get('client_id') != None and o['info']['client_id'] == customID ) 1282 | or o['clientOrderId'] == customID ): 1283 | id = o['id'] 1284 | elif( self.exchange.id == 'bybit' ): 1285 | id = None 1286 | params['orderLinkId'] = customID 1287 | elif( self.exchange.id == 'bingx' ): 1288 | id = None 1289 | params['clientOrderID'] = customID 1290 | else: 1291 | params['clientOrderId'] = customID 1292 | 1293 | 1294 | try: 1295 | response = self.exchange.cancel_order( id, symbol, params ) 1296 | 1297 | except Exception as e: 1298 | a = e.args[0] 1299 | if( isinstance(e, ccxt.OrderNotFound) or isinstance(e, ccxt.BadRequest) 1300 | or 'order not exists' in a ): 1301 | # ccxt.OrderNotFound: phemex, okx, kraken, binancedemo, bybit 1302 | # ccxt.BadRequest:kucoinfutures The order cannot be canceled 1303 | # coinex: order not exists (and that's all it says) 1304 | self.print( ' * E: Limit order [', customID, '] not found' ) 1305 | else: 1306 | self.print( ' * E: cancelLimitOrder:', e.args[0], type(e) ) 1307 | 1308 | else: 1309 | self.print( " * Linmit order [", customID, "] cancelled." ) 1310 | return True 1311 | 1312 | 1313 | def cancelAllOrders(self, symbol )->bool: 1314 | if( self.exchange.has.get('cancelAllOrders') ): 1315 | try: 1316 | response = self.exchange.cancel_all_orders(symbol) 1317 | except Exception as e: 1318 | # * E: cancelAllOrders: bitget {"code":"22001","msg":"No order to cancel","requestTime":1737926193912,"data":null} 1319 | if( 'code":"22001' in e.args[0] ): 1320 | self.print( 'cancelAllOrders: No orders found' ) 1321 | else: 1322 | self.print( ' * E: cancelAllOrders:', e.args[0], type(e) ) 1323 | # I've tried cancelling when there were no orders but it reported no error. Maybe I missed something. 1324 | else: 1325 | self.print( ' * All', symbol, 'orders have been cancelled' ) 1326 | return True 1327 | 1328 | try: 1329 | response = self.exchange.fetch_open_orders( symbol, params = {'settleCoin':self.SETTLE_COIN} ) 1330 | except Exception as e: 1331 | self.print( 'cancelAllOrders: No orders found', e.args[0], type(e) ) 1332 | return 1333 | 1334 | if( len(response) == 0 ): 1335 | self.print( 'cancelAllOrders: No orders found' ) 1336 | return 1337 | 1338 | cancelledCount = 0 1339 | for o in response: 1340 | if( o.get('symbol') == symbol ): 1341 | try: 1342 | response = self.exchange.cancel_order( o.get('id'), symbol ) 1343 | except Exception as e: 1344 | pass 1345 | else: 1346 | cancelledCount += 1 1347 | 1348 | self.print( 'cancelAllOrders:', cancelledCount, 'orders cancelled' ) 1349 | return True 1350 | 1351 | 1352 | 1353 | def updateOrdersQueue(self): 1354 | 1355 | # see if any active order was completed and delete it 1356 | while self.removeFirstCompletedOrder(): 1357 | continue 1358 | 1359 | if( len(self.ordersQueue) == 0 ): 1360 | return 1361 | 1362 | # go through the queue activating every symbol that doesn't have an active order 1363 | for order in self.ordersQueue: 1364 | if( self.activeOrderForSymbol(order.symbol) ): 1365 | continue 1366 | 1367 | if( order.timedOut() ): 1368 | self.print( timeNow(), " * Order Timed out", order.symbol, order.side, order.quantity, str(order.leverage)+'x' ) 1369 | self.ordersQueue.remove( order ) 1370 | continue 1371 | 1372 | if( order.delayed() ): 1373 | continue 1374 | 1375 | # disable hedge mode if present 1376 | self.updateSymbolPositionMode( order.symbol ) 1377 | 1378 | # see if the leverage in the server needs to be changed and set marginMode 1379 | if not debug_order: 1380 | self.updateSymbolLeverage( order.symbol, order.leverage ) 1381 | 1382 | if( order.side == 'changeleverage' ): 1383 | if( self.markets[ order.symbol ]['local']['leverage'] == order.leverage ): 1384 | self.print( " * Leverage changed to", self.markets[ order.symbol ]['local']['leverage'] ) 1385 | else: 1386 | self.print( " * E: Failed to change leverage." ) 1387 | self.ordersQueue.remove( order ) 1388 | continue 1389 | 1390 | 1391 | # set up exchange specific parameters 1392 | params = {} 1393 | 1394 | if( order.reduceOnly ): 1395 | params['reduce'] = True # FIXME Do we need this parameter? 1396 | if( self.exchange.id != 'coinex' ): # coinex interprets reduceOnly as being in hedge mode. Skip the problem by now 1397 | params['reduceOnly'] = True 1398 | 1399 | if( self.exchange.id == 'kucoinfutures' ): # Kucoin doesn't use setLeverage nor setMarginMode 1400 | params['leverage'] = max( order.leverage, 1 ) 1401 | params['marginMode'] = self.MARGIN_MODE 1402 | 1403 | if( self.exchange.id == 'krakenfutures' ): 1404 | params['leverage'] = max( order.leverage, 1 ) 1405 | params['marginMode'] = self.MARGIN_MODE 1406 | 1407 | if( self.exchange.id == 'okx' ): 1408 | params['leverage'] = max( order.leverage, 1 ) 1409 | params['marginMode'] = self.MARGIN_MODE 1410 | 1411 | if( self.exchange.id == 'bingx' ): 1412 | if( self.markets[ order.symbol ]['local']['positionMode'] == 'oneway' ): 1413 | params['positionSide'] = 'BOTH' 1414 | else: 1415 | params['positionSide'] = 'LONG' if( order.side == "buy" ) else 'SHORT' 1416 | 1417 | if( order.type == 'limit' ): 1418 | if( self.exchange.id == 'krakenfutures' ): 1419 | params['cliOrdId'] = order.customID 1420 | elif( self.exchange.id == 'coinex' ): 1421 | params['client_id'] = order.customID 1422 | elif( self.exchange.id == 'bingx' ): 1423 | params['clientOrderID'] = order.customID 1424 | else: 1425 | params['clientOrderId'] = order.customID 1426 | 1427 | # make sure it's precision adjusted properly 1428 | order.quantity = roundToTick( order.quantity, self.findPrecisionForSymbol(order.symbol) ) 1429 | 1430 | if debug_order: 1431 | price = account.fetchAveragePrice( order.symbol ) 1432 | print( timeNow(), " * Debug Order:", order.symbol, order.side, f": {(order.quantity * price)/float(order.leverage):.2f}$" ) 1433 | print( timeNow(), " * Debug Order:", f"{order.quantity} contracts", str(order.leverage)+'x' ) 1434 | self.ordersQueue.remove( order ) 1435 | continue 1436 | 1437 | # send the actual order 1438 | try: 1439 | response = self.exchange.create_order( order.symbol, order.type, order.side, order.quantity, order.price, params ) 1440 | #pprint( response ) 1441 | 1442 | except Exception as e: 1443 | a = e.args[0] 1444 | 1445 | if( isinstance(e, ccxt.InsufficientFunds) or '"code":"40762"' in a or 'code":101204' in a or '"code":-4131' in a 1446 | or 'code":101253' in a or 'balance not enough' in a ): 1447 | # coinex E: Cancelling: balance not enough 1448 | # KUCOIN: kucoinfutures Balance insufficient. The order would cost 304.7268292695. 1449 | # BITGET: {"code":"40754","msg":"balance not enough","requestTime":1689363604542,"data":null} 1450 | # bitget {"code":"40762","msg":"The order size is greater than the max open size","requestTime":1695925262092,"data":null} 1451 | # bingx {"code":101204,"msg":"Insufficient margin","data":{}} 1452 | # bingx {"code":101253,"msg":"Insufficient margin","data":{}} 1453 | # phemex {"code":11082,"msg":"TE_CANNOT_COVER_ESTIMATE_ORDER_LOSS","data":null} 1454 | # phemex {"code":11001,"msg":"TE_NO_ENOUGH_AVAILABLE_BALANCE","data":null} 1455 | # bybit {"retCode":140007,"retMsg":"remark:order[1643476 23006bb4-630a-4917-af0d-5412aaa1c950] fix price failed for CannotAffordOrderCost.","result":{},"retExtInfo":{},"time":1690540657794} 1456 | # bybit {"retCode":110007,"retMsg":"Insufficient available balance","result":{},"retExtInfo":{}," 1457 | # binance "code":-2019,"msg":"Margin is insufficient." 1458 | # krakenfutures: createOrder failed due to insufficientAvailableFunds 1459 | # binance {"code":-2027,"msg":"Exceeded the maximum allowable position at current leverage."} 1460 | # binance {"code":-4131,"msg":"The counterparty's best price does not meet the PERCENT_PRICE filter limit."} 1461 | # binance {"code":-4131,"msg":"The counterparty's best price does not meet the PERCENT_PRICE filter limit."} 1462 | precision = self.findPrecisionForSymbol( order.symbol ) 1463 | # try first reducing it to our estimation of current balance 1464 | 1465 | # This doesn't belong to insufficient funds, but cctx sends it here 1466 | if 'code":-4131' in a: 1467 | self.print( " * E: The counterparty's best price does not meet the PERCENT_PRICE filter limit. Retrying in 3 seconds" ) 1468 | order.delay += 2.0 1469 | 1470 | elif( not order.reduced ): 1471 | oldQuantity = order.quantity 1472 | price = self.fetchSellPrice(order.symbol) if( type == 'sell' ) else self.fetchBuyPrice(order.symbol) 1473 | available = self.fetchAvailableBalance() * 0.985 1474 | order.quantity = self.contractsFromUSDT( order.symbol, available, price, order.leverage ) 1475 | order.reduced = True 1476 | if( order.quantity < self.findMinimumAmountForSymbol(order.symbol) ): 1477 | self.print( ' * E: Balance insufficient: Minimum contracts required:', self.findMinimumAmountForSymbol(order.symbol), ' Cancelling') 1478 | self.ordersQueue.remove( order ) 1479 | else: 1480 | self.print( ' * E: Balance insufficient: Was', oldQuantity, 'Reducing to', order.quantity, "contracts") 1481 | 1482 | elif( order.quantity > precision ): 1483 | if( order.quantity < 20 and precision >= 1 ): 1484 | self.print( ' * E: Balance insufficient: Reducing by one contract') 1485 | order.quantity -= precision 1486 | else: 1487 | order.quantity = roundDownTick( order.quantity * 0.95, precision ) 1488 | if( order.quantity < self.findMinimumAmountForSymbol(order.symbol) ): 1489 | self.print( ' * E: Balance insufficient: Cancelling' ) 1490 | self.ordersQueue.remove( order ) 1491 | else: 1492 | self.print( ' * E: Balance insufficient: Reducing by 5%') 1493 | 1494 | else: # cancel the order 1495 | self.print( ' * E: Balance insufficient: Cancelling' ) 1496 | self.ordersQueue.remove( order ) 1497 | 1498 | continue # back to the orders loop 1499 | 1500 | 1501 | if( isinstance(e, ccxt.InvalidOrder) ): 1502 | # ERROR Cancelling: okx {"code":"1","data":[{"clOrdId":"001","ordId":"","sCode":"51006","sMsg":"Order price is not within the price limit (Maximum buy price: 26,899.6; minimum sell price: 25,844.6)","tag":""}],"inTime":"1695698840518495","msg":"","outTime":"1695698840518723"} 1503 | # bitget {"code":"45110","msg":"less than the minimum amount 5 USDT","requestTime":1719060978643,"data":null} 1504 | if 'Order price is not within' in a: 1505 | d = json.loads(a.lstrip(self.exchange.id + ' ')) 1506 | self.print( ' * E:', d['data'][0].get('sMsg') ) 1507 | self.ordersQueue.remove( order ) 1508 | elif 'invalidSize' in a or 'code":"45110' in a: 1509 | self.print( ' * E: Order size invalid:', order.quantity, 'x'+str(order.leverage) ) 1510 | self.ordersQueue.remove( order ) 1511 | elif '"retCode":20094' in a or '"code":-4015' in a or 'ID already exists' in a: 1512 | self.print( ' * E: Cancelling Linmit order: ID [', order.customID, '] was used before' ) 1513 | self.ordersQueue.remove( order ) 1514 | else: 1515 | self.print( ' * E: Invalid Order. Cancelling', e ) 1516 | self.ordersQueue.remove( order ) 1517 | 1518 | continue # back to the orders loop 1519 | 1520 | # bitget {"code":"22002","msg":"No position to close","requestTime":1765292553209,"data":null} 1521 | if 'No position' in a: 1522 | self.print( f'{order.symbol} No position to close."' ) 1523 | continue 1524 | 1525 | #HACK!! this is the shadiest hack ever, but bingx is returning a 'server busy' response 1526 | # when we try to place a limit order with a clientOrderID that has been already used. 1527 | # Basically, he's ghosting us!! It may have found it super offensive. 1528 | if( self.exchange.id == 'bingx' and order.type == 'limit' and '"code":101500' in a ): 1529 | self.print( ' * E: Cancelling Linmit order: ID [', order.customID, '] was used before' ) 1530 | self.ordersQueue.remove( order ) 1531 | continue 1532 | 1533 | 1534 | # bingx {"code":101500,"msg":"The current system is busy, please try again later","data":{}} 1535 | # bitget {"code":"400172","msg":"The order validity period is invalid","requestTime":1697878512831,"data":null} 1536 | # E: UpdateOrdersQueue: Unhandled exception. Cancelling: binance {"code":-1008,"msg":"Server is currently overloaded with other requests. Please try again in a few minutes."} 1537 | if( 'Too Many Requests' in a or 'too many request' in a 1538 | or 'service too busy' in a or 'system is busy' in a 1539 | or 'code":-1008' in a ): 1540 | #set a bigger delay and try again 1541 | order.delay += 1.0 1542 | print( " * Server too busy. Retrying.", type(e) ) 1543 | continue 1544 | 1545 | 1546 | # [bitget/bitget] bitget {"code":"45110","msg":"less than the minimum amount 5 USDT","requestTime":1689481837614,"data":null} 1547 | # The deviation between your delegated price and the index price is greater than 20%, you can appropriately adjust your delegation price and try again 1548 | self.print( ' * E: UpdateOrdersQueue: Unhandled exception. Cancelling:', a, type(e) ) 1549 | self.ordersQueue.remove( order ) 1550 | continue # back to the orders loop 1551 | 1552 | 1553 | if( response.get('id') == None ): 1554 | self.print( " * E: Order denied:", response['info'], "Cancelling" ) 1555 | self.ordersQueue.remove( order ) 1556 | continue # back to the orders loop 1557 | 1558 | order.id = response.get('id') 1559 | status = response.get('status') 1560 | remaining = response.get('remaining') 1561 | if( remaining != None and remaining > 0 and (status == 'canceled' or status == 'closed') ): 1562 | print("r...", end = '') 1563 | self.ordersQueue.append( order_c( order.symbol, order.side, remaining, order.leverage, 0.5 ) ) 1564 | self.ordersQueue.remove( order ) 1565 | continue 1566 | if( (remaining == None or remaining == 0) and (response.get('status') == 'closed' or response.get('status') == 'filled') ): 1567 | self.print( " * Order successful:", order.symbol, order.side, order.quantity, str(order.leverage)+"x", "at price", response.get('price'), 'id', order.id ) 1568 | self.ordersQueue.remove( order ) 1569 | continue 1570 | 1571 | if verbose : print( timeNow(), " * Activating Order", order.symbol, order.side, order.quantity, str(order.leverage)+'x', 'id', order.id ) 1572 | self.activeOrders.append( order ) 1573 | self.ordersQueue.remove( order ) 1574 | 1575 | 1576 | def proccessAlert( self, alert:dict ): 1577 | 1578 | self.print( ' ' ) 1579 | self.print( " ALERT:", alert['alert'] ) 1580 | self.print('----------------------------') 1581 | 1582 | # This is our first communication with the server, and (afaik) it will only fail when the server is not available. 1583 | # so we use it as a server availability check as well as for finding the available balance 1584 | try: 1585 | available = self.fetchAvailableBalance() * 0.985 1586 | except Exception as e: 1587 | a = e.args[0] 1588 | if( isinstance(e, ccxt.OnMaintenance) or isinstance(e, ccxt.NetworkError) 1589 | or isinstance(e, ccxt.RateLimitExceeded) or isinstance(e, ccxt.RequestTimeout) 1590 | or isinstance(e, ccxt.ExchangeNotAvailable) or 'not available' in a ): 1591 | # ccxt.base.errors.ExchangeError: Service is not available during funding fee settlement. Please try again later. 1592 | if( alert.get('timestamp') + ALERT_TIMEOUT < time.monotonic() ): 1593 | newAlert = copy.deepcopy( alert ) # the other alert will be deleted 1594 | if( isinstance(e, ccxt.RateLimitExceeded) ): 1595 | newAlert['delayTimestamp'] = time.monotonic() + 1 1596 | self.print( " * E: Rate limit exceeded. Retrying..." ) 1597 | else: 1598 | newAlert['delayTimestamp'] = time.monotonic() + 30 1599 | self.print( " * E: Couldn't reach the server: Retrying in 30 seconds", e, type(e) ) 1600 | self.latchedAlerts.append( newAlert ) 1601 | else: 1602 | self.print( " * E: Couldn't reach the server: Cancelling" ) 1603 | else: 1604 | self.print( " * E: Couldn't fetch balance: Cancelling", e, type(e) ) 1605 | return 1606 | 1607 | # 1608 | # TEMP: convert to the old vars. I'll change it later (maybe) 1609 | # 1610 | symbol = alert['symbol'] 1611 | command = alert['command'] 1612 | quantity = alert['quantity'] 1613 | leverage = alert['leverage'] if alert['leverage'] != 0 else self.markets[ symbol ]['local']['leverage'] 1614 | isUSDT = alert['isUSDT'] 1615 | isBaseCurrency = alert['isBaseCurrency'] 1616 | isPercentage = alert['isPercentage'] 1617 | lockBaseCurrency = alert['lockBaseCurrency'] 1618 | priceLimit = alert['priceLimit'] 1619 | customID = alert['customID'] 1620 | usdtValue = None 1621 | isLimit = True if priceLimit > 0.0 else False 1622 | 1623 | if( verbose ): 1624 | print( "PROCESSALERT: isUSDT:", isUSDT, "isBaseCurrency:", isBaseCurrency ) 1625 | 1626 | #time to put the order on the queue 1627 | 1628 | # No point in putting cancel orders in the queue. Just do it and leave. 1629 | if( command == 'cancel' ): 1630 | if( customID == 'all' ): 1631 | self.cancelAllOrders( symbol ) 1632 | else: 1633 | self.cancelLimitOrder( symbol, customID ) 1634 | return 1635 | 1636 | # bybit is too slow at updating positions after an order is made, so make sure they're updated 1637 | if( self.exchange.id == 'bybit' and (command == 'position' or command == 'close') ): 1638 | self.refreshPositions( False ) 1639 | 1640 | minOrder = self.findMinimumAmountForSymbol(symbol) 1641 | leverage = self.verifyLeverageRange( symbol, leverage ) 1642 | 1643 | # quantity is a percentage of the USDT balance 1644 | if( isPercentage and command != 'close' ): 1645 | quantity = min( max( float(quantity), -100.0 ), 100.0 ) 1646 | balance = float( self.fetchBalance().get( 'total' ) ) 1647 | quantity = round( balance * quantity * 0.01, 4 ) 1648 | isUSDT = True 1649 | 1650 | if quantity is not None and abs(quantity) < FLOAT_ERROR: 1651 | quantity = 0.0 1652 | 1653 | # convert quantity to concracts if needed 1654 | if( (isUSDT or isBaseCurrency) and quantity != 0.0 ) : 1655 | 1656 | # when using base currency with bclock, and contractsize is 1 we don't have to do any conversion 1657 | if not ( isBaseCurrency and lockBaseCurrency and self.findContractSizeForSymbol(symbol) == 1 ): 1658 | 1659 | # We don't know for sure yet if it's a buy or a sell, so we average 1660 | oldQuantity = quantity 1661 | try: 1662 | price = self.fetchAveragePrice(symbol) 1663 | 1664 | except ccxt.ExchangeError as e: 1665 | self.print( " * E: proccessAlert->fetchAveragePrice:", e ) 1666 | return 1667 | except ValueError as e: 1668 | self.print( " * E: proccessAlert->fetchAveragePrice", e, type(e) ) 1669 | return 1670 | 1671 | coin_name = self.markets[symbol]['quote'] 1672 | 1673 | if( isBaseCurrency ) : 1674 | if( lockBaseCurrency and leverage > 1 ): 1675 | quantity = quantity * price / leverage 1676 | else: 1677 | quantity *= price 1678 | 1679 | coin_name = self.markets[symbol]['base'] 1680 | 1681 | usdtValue = quantity 1682 | quantity = self.contractsFromUSDT( symbol, quantity, price, leverage ) 1683 | if verbose : print( " CONVERTING (x"+str(leverage)+")", oldQuantity, coin_name, '==>', quantity, "contracts" ) 1684 | 1685 | if( abs(quantity) < minOrder ): 1686 | self.print( " * E: Order too small:", quantity, "Minimum required:", minOrder ) 1687 | return 1688 | 1689 | # check for a existing position 1690 | pos = self.getPositionBySymbol( symbol ) 1691 | 1692 | if( command == 'changeleverage' ): 1693 | if( pos == None ): 1694 | self.print( " * E: No position to change leverage" ) 1695 | return 1696 | if( self.markets[ symbol ]['local']['leverage'] == leverage ): 1697 | self.print( " * Position already has leverage:", leverage ) 1698 | return 1699 | self.ordersQueue.append( order_c( symbol, 'changeleverage', leverage = leverage ) ) 1700 | return 1701 | 1702 | 1703 | if( command == 'close' or (command == 'position' and abs(quantity) < FLOAT_ERROR ) ): 1704 | if pos == None: 1705 | self.print( " * 'Close", symbol, "' No position found" ) 1706 | return 1707 | positionContracts = pos.getKey('contracts') 1708 | positionSide = pos.getKey( 'side' ) 1709 | 1710 | if( command == 'close' and isPercentage ): 1711 | quantity = min( abs(quantity), 100.0 ) 1712 | positionContracts = positionContracts * quantity * 0.01 1713 | 1714 | 1715 | if( positionSide == 'long' ): 1716 | self.ordersQueue.append( order_c( symbol, 'sell', positionContracts, 0 ) ) 1717 | else: 1718 | self.ordersQueue.append( order_c( symbol, 'buy', positionContracts, 0 ) ) 1719 | 1720 | return 1721 | 1722 | # position orders are absolute. Convert them to buy/sell order 1723 | if( command == 'position' ): 1724 | if( pos == None or pos.getKey('contracts') == None ): 1725 | # it's just a straight up buy or sell 1726 | if( quantity < 0 ): 1727 | command = 'sell' 1728 | else: 1729 | command = 'buy' 1730 | quantity = abs(quantity) 1731 | 1732 | # FIXME: Buy/sell commands can change marginmode in Bitget and retain the position. Maybe this isn't needed. 1733 | elif( self.markets[symbol]['local']['marginMode'] != self.MARGIN_MODE and self.exchange.has['setMarginMode'] ): 1734 | # to change marginMode we need to close the old position first 1735 | if( pos.getKey('side') == 'long' ): 1736 | self.ordersQueue.append( order_c( symbol, 'sell', pos.getKey('contracts'), 0 ) ) 1737 | else: 1738 | self.ordersQueue.append( order_c( symbol, 'buy', pos.getKey('contracts'), 0 ) ) 1739 | # Then create the order for the new position 1740 | if( quantity < 0 ): 1741 | command = 'sell' 1742 | else: 1743 | command = 'buy' 1744 | quantity = abs(quantity) 1745 | else: 1746 | # we need to account for the old position 1747 | positionContracts = pos.getKey('contracts') 1748 | positionSide = pos.getKey( 'side' ) 1749 | if( positionSide == 'short' ): 1750 | positionContracts = -positionContracts 1751 | 1752 | # !! We have to recalculate *from USDT* when the price is above the entry in a LONG and below the entry in a SHORT 1753 | if( usdtValue != None and positionSide == ("short" if quantity < 0.0 else "long") ): 1754 | extraMargin = 0 1755 | entryPrice = float(pos.getKey('entryPrice')) 1756 | initialMargin = (positionContracts * entryPrice)/float(self.markets[ symbol ]['local']['leverage']) 1757 | 1758 | if( initialMargin != -1 ): 1759 | #if we're going to change the leverage we need to manipulate the initial margen 1760 | if( leverage != self.markets[ symbol ]['local']['leverage'] ): 1761 | #if the new leverage is bigger the margin will be reduced 1762 | initialMargin = initialMargin * ( float(self.markets[ symbol ]['local']['leverage'] / float(leverage)) ) 1763 | 1764 | if( positionSide == 'long' ): 1765 | extraMargin = usdtValue - initialMargin 1766 | quantity = positionContracts + self.contractsFromUSDT( symbol, extraMargin, price, leverage ) 1767 | 1768 | elif( positionSide == 'short' ): 1769 | extraMargin = abs(usdtValue) - initialMargin 1770 | quantity = positionContracts - self.contractsFromUSDT( symbol, extraMargin, price, leverage ) 1771 | 1772 | 1773 | 1774 | command = 'sell' if positionContracts > quantity else 'buy' 1775 | quantity = abs( quantity - positionContracts ) 1776 | if( quantity < minOrder ): 1777 | # we don't need to buy nor sell, but do we need to change the leverage? 1778 | if( leverage != self.markets[ symbol ]['local']['leverage'] ): 1779 | self.ordersQueue.append( order_c( symbol, 'changeleverage', leverage = leverage ) ) 1780 | else: 1781 | self.print( " * Order completed: Request matched current position" ) 1782 | return 1783 | # if we are reducing the size and changing leverage we want to reduce size first, then modify the leverage 1784 | if( command == 'sell' and leverage != self.markets[ symbol ]['local']['leverage'] and self.markets[ symbol ]['local']['leverage'] != 0 ): # kucoin has 0 local leverage until an order is processed 1785 | alert = { 1786 | 'symbol': symbol, 1787 | 'command': 'changeleverage', 1788 | 'quantity': None, 1789 | 'leverage': leverage, 1790 | 'isUSDT': False, 1791 | 'isBaseCurrency': False, 1792 | 'isPercentage': False, 1793 | 'lockBaseCurrency': False, 1794 | 'priceLimit': 0.0, 1795 | 'customID': None, 1796 | 'alert': f"{symbol} changeleverage {leverage}", 1797 | 'timestamp':time.monotonic() 1798 | } 1799 | self.latchedAlerts.append( alert ) 1800 | leverage = self.markets[ symbol ]['local']['leverage'] # reduce the position with current leverage 1801 | # fall through 1802 | 1803 | 1804 | if( command == 'buy' or command == 'sell'): 1805 | 1806 | # fetch available balance and price 1807 | price = self.fetchSellPrice(symbol) if( command == 'sell' ) else self.fetchBuyPrice(symbol) 1808 | canDoContracts = self.contractsFromUSDT( symbol, available, price, leverage ) 1809 | 1810 | if( pos != None ): 1811 | positionContracts = pos.getKey('contracts') 1812 | positionSide = pos.getKey( 'side' ) 1813 | 1814 | # reversing the position 1815 | if not isLimit and (( positionSide == 'long' and command == 'sell' ) or ( positionSide == 'short' and command == 'buy' )): 1816 | 1817 | # do we need to divide these in 2 orders? 1818 | 1819 | # bingx must make one order for close and a second one for the new position 1820 | if( self.exchange.id == 'bingx' ): 1821 | if( quantity > positionContracts ): 1822 | self.ordersQueue.append( order_c( symbol, command, positionContracts, 0 ) ) 1823 | quantity -= positionContracts 1824 | self.ordersQueue.append( order_c( symbol, command, quantity, leverage ) ) 1825 | return 1826 | 1827 | self.ordersQueue.append( order_c( symbol, command, quantity, leverage, reduceOnly=True ) ) 1828 | return 1829 | 1830 | # FIXME: Bybit takes the fees on top of the order which makes it fail with insuficcient 1831 | # balance when we try to order all the balance at once, which creates complications 1832 | # when reducing a reveral order. This is a temporary way to make it work, but 1833 | # we should really calculate the fees 1834 | # 1835 | # FIXME: Temporarily using this path for OKX too 1836 | if( ( self.exchange.id == 'bybit' or self.exchange.id == 'okx' ) and quantity > positionContracts ): 1837 | self.ordersQueue.append( order_c( symbol, command, positionContracts, 0, reduceOnly = True ) ) 1838 | quantity -= positionContracts 1839 | if( quantity > minOrder ): 1840 | self.ordersQueue.append( order_c( symbol, command, quantity, leverage ) ) 1841 | return 1842 | 1843 | if( quantity >= canDoContracts + positionContracts ): 1844 | # we have to make sure each of the orders has the minimum order contracts 1845 | order1 = canDoContracts + positionContracts 1846 | order2 = quantity - (canDoContracts + positionContracts) 1847 | if( order2 < minOrder ): 1848 | diff = minOrder - order2 1849 | if( order1 > minOrder + diff ): 1850 | order1 -= diff 1851 | 1852 | # first order is the contracts in the position and the contracs we can afford with the liquidity 1853 | self.ordersQueue.append( order_c( symbol, command, order1, leverage ) ) 1854 | 1855 | # second order is whatever we can afford with the former position contracts + the change 1856 | quantity -= order1 1857 | if( quantity >= minOrder ): #we are done (should never happen) 1858 | self.ordersQueue.append( order_c( symbol, command, quantity, leverage, 1.0 ) ) 1859 | 1860 | return 1861 | # fall through 1862 | 1863 | if( quantity < minOrder ): 1864 | self.print( timeNow(), " * E: Order too small:", quantity, "Minimum required:", minOrder ) 1865 | return 1866 | 1867 | order = order_c( symbol, command, quantity, leverage ) 1868 | if( isLimit ): 1869 | order.type = 'limit' 1870 | order.customID = customID 1871 | order.price = priceLimit 1872 | 1873 | self.ordersQueue.append( order ) 1874 | return 1875 | 1876 | self.print( " * E: Something went wrong. No order was placed") 1877 | 1878 | 1879 | accounts = [] 1880 | 1881 | 1882 | 1883 | 1884 | def stringToValue( arg )->float: 1885 | try: 1886 | float(arg) 1887 | except ValueError: 1888 | value = None 1889 | else: 1890 | value = float(arg) 1891 | return value 1892 | 1893 | 1894 | def updateOrdersQueue(): 1895 | for account in accounts: 1896 | numOrders = len(account.ordersQueue) + len(account.activeOrders) 1897 | account.updateOrdersQueue() 1898 | 1899 | # see if we have any alert pending to be proccessed 1900 | if( len(account.latchedAlerts) ): 1901 | positionsRefreshed = False 1902 | for alert in account.latchedAlerts: 1903 | if( alert.get('delayTimestamp') != None ): 1904 | alert.get('delayTimestamp') < time.monotonic() 1905 | continue 1906 | 1907 | busy = False 1908 | for order in account.activeOrders: 1909 | if( order.symbol == alert['symbol'] ): 1910 | busy = True 1911 | break 1912 | for order in account.ordersQueue: 1913 | if( order.symbol == alert['symbol'] ): 1914 | busy = True 1915 | break 1916 | 1917 | if( not busy ): 1918 | if( not positionsRefreshed ): 1919 | account.refreshPositions(False) 1920 | positionsRefreshed = True 1921 | 1922 | account.proccessAlert( alert ) 1923 | account.latchedAlerts.remove( alert ) 1924 | 1925 | # if we just cleared the orders queue refresh the positions info 1926 | if( numOrders > 0 and (len(account.ordersQueue) + len(account.activeOrders)) == 0 ): 1927 | account.refreshPositions(True) 1928 | 1929 | 1930 | def refreshPositions(): 1931 | for account in accounts: 1932 | account.refreshPositions() 1933 | 1934 | 1935 | def generatePositionsString()->str: 1936 | msg = '' 1937 | for account in accounts: 1938 | account.refreshPositions() 1939 | numPositions = len(account.positionslist) 1940 | balanceString = '' 1941 | if SHOW_BALANCE: 1942 | try: 1943 | balance = account.fetchBalance() 1944 | except Exception as e: 1945 | balanceString = '' 1946 | else: 1947 | balanceString = " * Balance: {:.2f}[$]".format(balance['total']) 1948 | balanceString += " - Available {:.2f}[$]".format(balance['free']) 1949 | 1950 | msg += '---------------------\n' 1951 | msg += 'Refreshing positions '+account.accountName+': ' + str(numPositions) + ' positions found' + balanceString + '\n' 1952 | if( numPositions == 0 ): 1953 | continue 1954 | 1955 | for pos in account.positionslist: 1956 | msg += pos.generatePrintString() + '\n' 1957 | 1958 | return msg 1959 | 1960 | def parseAlert( data, account: account_c ): 1961 | 1962 | if( account == None ): 1963 | return { 'Error': " * E: parseAlert called without an account" } 1964 | 1965 | alert = { 1966 | 'symbol': None, 1967 | 'command': None, 1968 | 'quantity': None, 1969 | 'leverage': 0, 1970 | 'isUSDT': False, 1971 | 'isBaseCurrency': False, 1972 | 'isPercentage': False, 1973 | 'lockBaseCurrency': False, 1974 | 'priceLimit': 0.0, 1975 | 'customID': None, 1976 | 'alert': data, 1977 | 'timestamp':time.monotonic() 1978 | } 1979 | 1980 | limitToken = None 1981 | cancelToken = None 1982 | 1983 | # Informal plain text syntax 1984 | tokens = data.split() 1985 | for token in tokens: 1986 | if( account.findSymbolFromPairName(token) != None ): # GMXUSDTM, GMX/USDT:USDT and GMX/USDT are all acceptable formats 1987 | alert['symbol'] = account.findSymbolFromPairName(token) 1988 | elif ( token.lower() == account.accountName.lower() ): 1989 | pass 1990 | elif ( token[:1].lower() == "$" or token[-1:] == "$" ): # value in USDT 1991 | alert['isUSDT'] = True 1992 | arg = token.lower().strip().replace("$", "") 1993 | alert['quantity'] = stringToValue( arg ) 1994 | elif ( token[:1].lower() == "@" or token[-1:] == "@" ): # value in contracts 1995 | arg = token.lower().strip().replace("@", "") 1996 | alert['quantity'] = stringToValue( arg ) 1997 | elif ( token[:1].lower() == "%" or token[-1:] == "%" ): # value in percentage of balance 1998 | arg = token.lower().strip().replace("%", "") 1999 | alert['quantity'] = stringToValue( arg ) 2000 | alert['isPercentage'] = True 2001 | elif ( token[:1] == "-" ): # this is a minus symbol! What a bitch (value in base currency) 2002 | alert['isBaseCurrency'] = True 2003 | alert['quantity'] = stringToValue( token ) 2004 | elif ( stringToValue( token ) != None ): 2005 | alert['isBaseCurrency'] = True 2006 | arg = token 2007 | alert['quantity'] = stringToValue(arg) 2008 | elif token.lower() == 'force_usdt': 2009 | alert['isUSDT'] = True 2010 | elif token.lower() == 'force_percent': 2011 | alert['isPercentage'] = True 2012 | elif token.lower() == 'force_basecurrency': 2013 | alert['isBaseCurrency'] = True 2014 | elif token.lower() == 'lockbasecurrency' or token.lower() == "bclock": 2015 | alert['lockBaseCurrency'] = True 2016 | elif ( token[:1].lower() == "x" or token[-1:].lower() == "x"): 2017 | arg = token.lower().strip().replace("x", "") 2018 | leverage = stringToValue(arg) 2019 | alert['leverage'] = int(leverage) if leverage is not None else 0 2020 | elif token.lower() == 'long': 2021 | alert['command'] = 'buy' 2022 | print( "WARNING: 'long' and 'short' commands are deprecated and will be removed in the future. Please use 'buy' and 'sell' instead" ) 2023 | elif token.lower() == 'short': 2024 | alert['command'] = 'sell' 2025 | print( "WARNING: 'long' and 'short' commands are deprecated and will be removed in the future. Please use 'buy' and 'sell' instead" ) 2026 | elif token.lower() == "buy": 2027 | alert['command'] = 'buy' 2028 | elif token.lower() == "sell": 2029 | alert['command'] = 'sell' 2030 | elif token.lower() == 'close': 2031 | alert['command'] = 'close' 2032 | elif token.lower() == 'position' or token.lower() == 'pos': 2033 | alert['command'] = 'position' 2034 | elif token.lower() == 'changeleverage': 2035 | alert['command'] = 'changeleverage' 2036 | elif ( token[:5].lower() == "limit" ): 2037 | limitToken = token # we validate it at processing 2038 | elif ( token[:6].lower() == "cancel" ): 2039 | cancelToken = token # we validate it at processing 2040 | alert['command'] = 'cancel' 2041 | else: 2042 | print( "Unknown alert command:", token ) 2043 | 2044 | if( alert['isPercentage'] ): 2045 | alert['isBaseCurrency'] = False 2046 | alert['isUSDT'] = False 2047 | if( alert['isUSDT'] ): 2048 | alert['isBaseCurrency'] = False 2049 | 2050 | # do some syntax validation 2051 | if( alert['symbol'] == None ): 2052 | return { 'Error': " * E: Couldn't find symbol" } 2053 | 2054 | if( alert['command'] == None ): 2055 | return { 'Error': " * E: Invalid Order: Missing command" } 2056 | 2057 | if( alert['command'] == 'buy' or alert['command'] == 'sell' or alert['command'] == 'position' ): 2058 | if( alert['quantity'] == None ): 2059 | return { 'Error': " * E: Invalid quantity value" } 2060 | if( alert['quantity'] < 0 and alert['command'] == 'buy' ): 2061 | return { 'Error': " * E: Invalid Order: Buy must have a positive amount" } 2062 | if( alert['quantity'] == 0 and alert['command'] != 'position' ): 2063 | return { 'Error':" * E: Invalid Order amount: 0" } 2064 | if( alert['command'] == 'sell' and alert['quantity'] < 0 ): # be flexible with sell having a negative amount 2065 | alert['quantity'] = abs(alert['quantity']) 2066 | 2067 | if( alert['command'] == "changeleverage" ): 2068 | alert['isBaseCurrency'] = False 2069 | alert['isUSDT'] = False 2070 | alert['isBaseCurrency'] = False 2071 | if( alert['quantity'] == None ): 2072 | alert['quantity'] = alert['leverage'] 2073 | if( alert['quantity'] == 0 ): 2074 | return { 'Error': " * E: Couldn't find a leverage value for setleverage" } 2075 | if( alert['leverage'] == 0 ): 2076 | alert['leverage'] = int( alert['quantity'] ) 2077 | 2078 | # parse de cancel and limit tokens 2079 | if( limitToken != None ): 2080 | if( alert['command'] != 'buy' and alert['command'] != 'sell' ): 2081 | return { 'Error': " * E: Limit orders can only be used with buy/sell commands" } 2082 | 2083 | v = limitToken.split(':') 2084 | if( len(v) != 3 ): 2085 | return { 'Error': " * E: Limit command must be formatted as 'limit:customID:price' " } 2086 | else: 2087 | alert['customID'] = v[1] 2088 | alert['priceLimit'] = stringToValue(v[2]) 2089 | if( alert['priceLimit'] == None ): 2090 | return { 'Error': " * E: Limit command must be formatted as 'limit:customID:price' " } 2091 | if( alert['priceLimit'] <= 0 ): 2092 | return { 'Error': " * E: price limit must be bigger than 0" } 2093 | 2094 | if ( cancelToken != None ): 2095 | v = cancelToken.split(':') 2096 | if( len(v) != 2 ): 2097 | return { 'Error': " * E: Cancel command must be formatted as 'cancel:customID' " } 2098 | alert['customID'] = v[1] 2099 | 2100 | if( alert['customID'] != None ): 2101 | if( len(alert['customID']) < 2 or len(alert['customID']) > 30 ): 2102 | return { 'Error': " * E: customID must be longer than 2 characters and shorter than 30' " } 2103 | if( account.exchange.id == 'coinex' and not alert['customID'].isdigit() ): 2104 | return { 'Error': " * E: Coinex only accepts numeric customID' " } 2105 | 2106 | if verbose : print( alert ) 2107 | return alert 2108 | 2109 | 2110 | 2111 | def Alert( data ): 2112 | 2113 | account = None 2114 | 2115 | # first lets find out if there's more than one commands inside the alert message 2116 | lines = data.split("\n") 2117 | for line in lines: 2118 | line = line.rstrip('\n') 2119 | if( len(line) == 0 ): 2120 | continue 2121 | if( line[:2] == '//' ): # if the line begins with // it's a comment and we skip it 2122 | continue 2123 | account = None 2124 | tokens = line.split() 2125 | for token in tokens: 2126 | for a in accounts: 2127 | if( token.lower() == a.accountName.lower() ): 2128 | account = a 2129 | break 2130 | if( account == None ): 2131 | print( timeNow(), ' * E: Account ID not found. ALERT:', line ) 2132 | continue 2133 | 2134 | alert = parseAlert( line.replace('\n', ''), account ) 2135 | if( alert.get('Error') != None ): 2136 | account.print( ' ' ) 2137 | account.print( " ALERT:", line.replace('\n', '') ) 2138 | account.print('----------------------------') 2139 | account.print( alert.get('Error') ) 2140 | continue 2141 | 2142 | # check if the alert can be proccessed inmediately 2143 | busy = False 2144 | for o in account.activeOrders: 2145 | if( o.symbol == alert['symbol'] ): 2146 | busy = True 2147 | break 2148 | for o in account.ordersQueue: 2149 | if( o.symbol == alert['symbol'] ): 2150 | busy = True 2151 | break 2152 | 2153 | if( not busy ): 2154 | account.proccessAlert( alert ) 2155 | continue 2156 | 2157 | # delay the alert proccessing 2158 | account.latchedAlerts.append( alert ) 2159 | 2160 | 2161 | 2162 | ################### 2163 | #### Initialize ### 2164 | ################### 2165 | 2166 | print('----------------------------') 2167 | 2168 | #### Open accounts file ### 2169 | 2170 | try: 2171 | with open('accounts.json', 'r') as accounts_file: 2172 | accounts_data = json.load(accounts_file) 2173 | accounts_file.close() 2174 | except FileNotFoundError: 2175 | with open('accounts.json', 'x') as f: 2176 | f.write( '[\n\t{\n\t\t"ACCOUNT_ID":"your_account_name", \n\t\t"EXCHANGE":"exchange_name", \n\t\t"API_KEY":"your_api_key", \n\t\t"SECRET_KEY":"your_secret_key", \n\t\t"PASSWORD":"your_API_password", \n\t\t"MARGIN_MODE":"isolated"\n\t}\n]' ) 2177 | f.close() 2178 | print( "File 'accounts.json' not found. Template created. Please fill your API Keys into the file and try again") 2179 | print( "Exiting." ) 2180 | raise SystemExit() 2181 | 2182 | for ac in accounts_data: 2183 | 2184 | exchange = ac.get('EXCHANGE') 2185 | if( exchange == None ): 2186 | print( " * ERROR PARSING ACCOUNT INFORMATION: EXCHANGE" ) 2187 | continue 2188 | 2189 | account_id = ac.get('ACCOUNT_ID') 2190 | if( account_id == None ): 2191 | print( " * ERROR PARSING ACCOUNT INFORMATION: ACCOUNT_ID" ) 2192 | continue 2193 | 2194 | api_key = ac.get('API_KEY') 2195 | if( api_key == None ): 2196 | print( " * ERROR PARSING ACCOUNT INFORMATION: API_KEY" ) 2197 | continue 2198 | 2199 | secret_key = ac.get('SECRET_KEY') 2200 | if( secret_key == None ): 2201 | print( " * ERROR PARSING ACCOUNT INFORMATION: SECRET_KEY" ) 2202 | continue 2203 | 2204 | password = ac.get('PASSWORD') 2205 | if( password == None ): 2206 | password = "" 2207 | continue 2208 | 2209 | marginMode = ac.get('MARGIN_MODE') 2210 | 2211 | settleCoin = ac.get('SETTLE_COIN') 2212 | 2213 | print( timeNow(), " Initializing account: [", account_id, "] in [", exchange , ']') 2214 | try: 2215 | account = account_c( exchange, account_id, api_key, secret_key, password, marginMode, settleCoin ) 2216 | except Exception as e: 2217 | print( 'Account creation failed:', e, type(e) ) 2218 | print('------------------------------') 2219 | else: 2220 | accounts.append( account ) 2221 | 2222 | if( len(accounts) == 0 ): 2223 | print( " * FATAL ERROR: No valid accounts found. Please edit 'accounts.json' and introduce your API keys" ) 2224 | raise SystemExit() 2225 | 2226 | 2227 | ############################################ 2228 | 2229 | # define the webhook server 2230 | app = Flask(__name__) 2231 | # silencing flask useless spam 2232 | log = logging.getLogger('werkzeug') 2233 | log.setLevel(logging.ERROR) 2234 | log.disabled = True 2235 | 2236 | if USE_PROXY == True: 2237 | # warn Flask that we are behind a Web proxy 2238 | app.wsgi_app = ProxyFix( 2239 | app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1 2240 | ) 2241 | PORT = PROXY_PORT 2242 | 2243 | @app.route('/whook', methods=['GET','POST']) 2244 | def webhook(): 2245 | 2246 | if request.method == 'POST': 2247 | content_type = request.headers.get('Content-Type') 2248 | if content_type == 'application/json': 2249 | data = request.get_json() 2250 | 2251 | if data and 'update_id' in data: # Typical key in Telegram bot updates 2252 | # Extract message text and chat ID 2253 | if 'message' in data: 2254 | chat_id = data['message']['chat']['id'] 2255 | message = data['message']['text'] 2256 | # Log the received message 2257 | print( "Received message from chat_id", chat_id, ':', message ) 2258 | return 'Telegram message processed', 200 2259 | 2260 | # we received a json of unknown source 2261 | return 'success', 200 2262 | 2263 | # Standard alert 2264 | data = request.get_data(as_text=True) 2265 | Alert(data) 2266 | return 'success', 200 2267 | 2268 | if request.method == 'GET': 2269 | # https://0.0.0.0/whook 2270 | response = request.args.get('response') 2271 | if( response == None ): 2272 | fontSize = 18 2273 | if fontSize > 0: 2274 | msg = f""" 2275 | 2276 | 2277 | 2278 | Positions 2279 | 2280 | 2281 |
{generatePositionsString()}
2282 | 2283 | 2284 | """ 2285 | return app.response_class( msg, mimetype='text/html; charset=utf-8' ) 2286 | else: 2287 | msg = generatePositionsString() 2288 | return app.response_class( msg, mimetype='text/plain; charset=utf-8' ) 2289 | 2290 | if response == 'whook': 2291 | return 'WHOOKITYWOOK' 2292 | 2293 | # https://0.0.0.0/whook?response=account 2294 | if response.lower() == 'allaccounts': 2295 | package = {"allaccounts": {}} 2296 | for acc in accounts: 2297 | acc.refreshPositions(False) 2298 | package["allaccounts"][acc.accountName] = { "positions": [pos.generateDictionary() for pos in acc.positionslist], 2299 | "balance": acc.fetchBalance().get('total') } 2300 | return jsonify(package) 2301 | else: 2302 | package = {"allaccounts": {}} 2303 | for acc in accounts: 2304 | if acc.accountName.lower() == response.lower(): 2305 | acc.refreshPositions(False) 2306 | package["allaccounts"][acc.accountName] = { "positions": [pos.generateDictionary() for pos in acc.positionslist], 2307 | "balance": acc.fetchBalance().get('total') } 2308 | return jsonify(package) 2309 | 2310 | # temporarily disabled. 2311 | # Return the requested log file 2312 | # try: 2313 | # wmsg = open( f'{LOGS_DIRECTORY}/{response}.log', encoding="utf-8" ) 2314 | # except FileNotFoundError: 2315 | # return 'Not found' 2316 | # else: 2317 | # text = wmsg.read() 2318 | # wmsg.close() 2319 | # return app.response_class(text, mimetype='text/plain; charset=utf-8') 2320 | 2321 | else: 2322 | abort(400) 2323 | 2324 | # start the positions fetching loop 2325 | timerFetchPositions = RepeatTimer( REFRESH_POSITIONS_FREQUENCY, refreshPositions ) 2326 | timerFetchPositions.start() 2327 | 2328 | timerOrdersQueue = RepeatTimer( UPDATE_ORDERS_FREQUENCY, updateOrdersQueue ) 2329 | timerOrdersQueue.start() 2330 | 2331 | # start the webhook server 2332 | if __name__ == '__main__': 2333 | print( " * Listening" ) 2334 | app.run(host="0.0.0.0", port=PORT, debug=False) 2335 | 2336 | 2337 | --------------------------------------------------------------------------------