├── thumb.png ├── compile_nuitka.cmd ├── .gitignore ├── requirements.txt ├── native.py ├── LICENSE ├── log.py ├── auth.py ├── config.json ├── README.md └── qapi.py /thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hclivess/qtrader/HEAD/thumb.png -------------------------------------------------------------------------------- /compile_nuitka.cmd: -------------------------------------------------------------------------------- 1 | python -m nuitka qapi.py --standalone --show-progress -j 8 --recurse-all -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | secret 2 | .idea/ 3 | *.log 4 | *.log.1 5 | *.log.2 6 | *.log.3 7 | *.log.4 8 | *.log.5 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | python-dateutil 2 | pip3 install --upgrade --user git+https://github.com/qtrade-exchange/qtrade-py-client.git -------------------------------------------------------------------------------- /native.py: -------------------------------------------------------------------------------- 1 | from qtrade_client.api import QtradeAPI 2 | 3 | 4 | def load_credentials(): 5 | with open("secret") as authfile: 6 | return authfile.read() 7 | 8 | 9 | # String is of the format "[key_id]:[key]" 10 | client_native = QtradeAPI("https://api.qtrade.io", key=load_credentials()) 11 | 12 | # result = client.post("/v1/user/sell_limit", amount="1", price="0.0001", market_id=12) 13 | # print(result) 14 | 15 | # Only closed orders 16 | print(client_native.orders(open=False)) 17 | # Print all orders before ID 25 18 | print(client_native.orders(older_than=25)) 19 | # Print all orders after ID 25 20 | print(client_native.orders(newer_than=25)) 21 | client_native.balances() -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Jan Kučera 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 | -------------------------------------------------------------------------------- /log.py: -------------------------------------------------------------------------------- 1 | import logging.handlers 2 | 3 | 4 | class Logger: 5 | def __init__(self, filename="log.log", level_string="INFO"): 6 | 7 | if level_string == "DEBUG": 8 | self.level = logging.DEBUG 9 | elif level_string == "INFO": 10 | self.level = logging.INFO 11 | elif level_string == "WARNING": 12 | self.level = logging.WARNING 13 | elif level_string == "ERROR": 14 | self.level = logging.ERROR 15 | else: 16 | self.level = logging.NOTSET 17 | 18 | self.logger = self.define_logger(filename) 19 | 20 | self.logger.info(f"Logging set to {self.level}, {level_string}") 21 | 22 | def define_logger(self, filename): 23 | logFormatter = logging.Formatter("%(asctime)s [%(threadName)-12.12s] [%(levelname)-5.5s] %(message)s") 24 | rootLogger = logging.getLogger() 25 | rootLogger.setLevel(self.level) 26 | fileHandler = logging.FileHandler(filename) 27 | fileHandler.setFormatter(logFormatter) 28 | rootLogger.addHandler(fileHandler) 29 | consoleHandler = logging.StreamHandler() 30 | consoleHandler.setFormatter(logFormatter) 31 | rootLogger.addHandler(consoleHandler) 32 | return rootLogger 33 | -------------------------------------------------------------------------------- /auth.py: -------------------------------------------------------------------------------- 1 | import requests.auth 2 | from hashlib import sha256 3 | from urllib.parse import urlparse 4 | import time 5 | import base64 6 | 7 | 8 | class QtradeAuth(requests.auth.AuthBase): 9 | def __init__(self, key): 10 | self.key_id, self.key = key.split(":") 11 | 12 | def __call__(self, req): 13 | # modify and return the request 14 | timestamp = str(int(time.time())) 15 | url_obj = urlparse(req.url) 16 | 17 | request_details = req.method + "\n" 18 | request_details += url_obj.path + url_obj.params + "\n" 19 | request_details += timestamp + "\n" 20 | 21 | if req.body: 22 | if isinstance(req.body, str): 23 | request_details += req.body + "\n" 24 | else: 25 | request_details += req.body.decode("utf8") + "\n" 26 | else: 27 | request_details += "\n" 28 | request_details += self.key 29 | hsh = sha256(request_details.encode("utf8")).digest() 30 | signature = base64.b64encode(hsh) 31 | req.headers.update( 32 | { 33 | "Authorization": f"HMAC-SHA256 {self.key_id}:{signature.decode()}", 34 | "HMAC-Timestamp": timestamp, 35 | } 36 | ) 37 | return req 38 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "BIS", 4 | "sell_amount": "150", 5 | "min_sell_price": "0.00001400", 6 | "buy_amount": "150", 7 | "max_buy_price": "0.00002000", 8 | "buy_longevity": "360", 9 | "sell_longevity": "120", 10 | "spread_pct_min": "1", 11 | "price_adjustment": "0.00000001", 12 | "max_stash": "50000", 13 | "min_stash": "25000", 14 | "random_size": "5", 15 | "end_pause": "3" 16 | }, 17 | { 18 | "name": "NYZO", 19 | "sell_amount": "50", 20 | "min_sell_price": "0.00002000", 21 | "buy_amount": "50", 22 | "max_buy_price": "0.00003000", 23 | "buy_longevity": "360", 24 | "sell_longevity": "120", 25 | "spread_pct_min": "1", 26 | "price_adjustment": "0.00000001", 27 | "max_stash": "10000", 28 | "min_stash": "1000", 29 | "random_size": "5", 30 | "end_pause": "3" 31 | }, 32 | { 33 | "name": "VEO", 34 | "sell_amount": "0.15", 35 | "min_sell_price": "0.005", 36 | "buy_amount": "0.1", 37 | "max_buy_price": "0.15", 38 | "buy_longevity": "120", 39 | "sell_longevity": "120", 40 | "spread_pct_min": "1", 41 | "price_adjustment": "0.00000001", 42 | "max_stash": "10", 43 | "min_stash": "0.1", 44 | "random_size": "0.005", 45 | "end_pause": "3" 46 | }, 47 | { 48 | "name": "ARO", 49 | "sell_amount": "1500", 50 | "min_sell_price": "0.00000010", 51 | "buy_amount": "1500", 52 | "max_buy_price": "0.00000015", 53 | "buy_longevity": "120", 54 | "sell_longevity": "120", 55 | "spread_pct_min": "1", 56 | "price_adjustment": "0.00000000", 57 | "max_stash": "20000", 58 | "min_stash": "10000", 59 | "random_size": "20", 60 | "end_pause": "3" 61 | }, 62 | { 63 | "name": "PASC", 64 | "sell_amount": "100", 65 | "min_sell_price": "0.00000700", 66 | "buy_amount": "100", 67 | "max_buy_price": "0.00001000", 68 | "buy_longevity": "120", 69 | "sell_longevity": "120", 70 | "spread_pct_min": "1", 71 | "price_adjustment": "0.00000001", 72 | "max_stash": "2000", 73 | "min_stash": "0", 74 | "random_size": "20", 75 | "end_pause": "3" 76 | }, 77 | { 78 | "name": "SNOW", 79 | "sell_amount": "200", 80 | "min_sell_price": "0.00002500", 81 | "buy_amount": "200", 82 | "max_buy_price": "0.00003500", 83 | "buy_longevity": "120", 84 | "sell_longevity": "120", 85 | "spread_pct_min": "1", 86 | "price_adjustment": "0.00000001", 87 | "max_stash": "2000", 88 | "min_stash": "0", 89 | "random_size": "20", 90 | "end_pause": "3" 91 | } 92 | ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # qTrader 2 | Welcome to the qTrade trading bot. 3 | 4 | ### What does this bot offer? 5 | This bot places buy and sell orders for you at the best possible price, and at the best possible exposure. 6 | 7 | ### What is the premise? 8 | Market spread is an inefficiency, which can be exploited by covering both sides of it while the difference is higher than market fees. Nothing new. 9 | 10 | ### How do I run this bot? 11 | Run `qapi.py` in Python3. On start, you will be asked for your API credentials. You need to obtain these from [https://qtrade.io/settings/api_keys](https://qtrade.io/settings/api_keys) 12 | There is a configuration file `config.json` where you set your trading parameters. 13 | 14 | ### Config File 15 | Bot configuration is set at `config.json` file. 16 | Buy orders are placed on top of the buy order book, with an increase of `price_adjustment`. 17 | 18 | Sell orders are placed on top of the sell order book, with a decrease of `price_adjustment`. 19 | 20 | The minimum spread between orders is defined by `spread_pct_min`. 21 | 22 | Order book recalculation is set by `ttl` in seconds. However, pauses between runs are set at 90 seconds static. 23 | 24 | ### What returns can I expect? 25 | 26 | The bot profits the most from a stable price range with a high spread. It works actively towards lowering the spread in order to beat other trading bots and humans. 27 | 28 | ### What should I be aware of? 29 | 30 | This bot does **not** track individual order performance, because that would require static orders. Static orders are not favorable due to their inert nature, which results in them getting stuck forever outside the price range. 31 | 32 | A very strong trend where there is only one type of trading going on (buys / sells only), this bot cannot operate properly. However, such situation should only be temporary. 33 | 34 | ### Tips 35 | 36 | In an uptrend scenario, it might be a good idea to have a higher `sell_amount` than `buy_amount` in magnitude strong enough to knock down the price back to your buy orders. 37 | The opposite is true for a downtrend. 38 | 39 | ### Pre-requisites 40 | 41 | #### Linux 42 | 43 | ``` 44 | sudo apt-get install python3.7 45 | python3.7 -m pip install python-dateutil 46 | python3.7 -m pip install --upgrade --user git+https://github.com/qtrade-exchange/qtrade-py-client.git` 47 | ``` 48 | 49 | #### Windows 50 | 51 | Install Python 3.7 52 | 53 | cmd (admin) 54 | ``` 55 | python -m pip install python-dateutil 56 | python -m pip install --upgrade --user git+https://github.com/qtrade-exchange/qtrade-py-client.git 57 | ``` 58 | 59 | 60 | ### Configuration example 61 | [Available here](config.json) 62 | 63 | ![alt text](thumb.png "Thumbnail") -------------------------------------------------------------------------------- /qapi.py: -------------------------------------------------------------------------------- 1 | from qtrade_client.api import QtradeAPI 2 | import random 3 | import json 4 | import time 5 | import os 6 | import sys 7 | from log import Logger 8 | from datetime import datetime 9 | 10 | import requests 11 | from dateutil import parser 12 | 13 | from auth import QtradeAuth 14 | 15 | DEBUG = False 16 | DEMO = False 17 | DEMO_MARKETS = ["NYZO", "BIS"] 18 | 19 | log_obj = Logger("qtrader.log", "WARNING") 20 | log = log_obj.logger 21 | 22 | 23 | def part_percentage(part, whole): 24 | return float(100.0) * part / whole 25 | 26 | 27 | def load_credentials(): 28 | if os.path.exists("secret"): 29 | with open("secret") as authfile: 30 | return authfile.read() 31 | else: 32 | while True: 33 | with open("secret", "w") as authfile: 34 | auth = input("Enter your qTrade authentication data: ") 35 | if ":" in auth: 36 | authfile.write(auth) 37 | return auth 38 | else: 39 | pass 40 | 41 | 42 | class Order: 43 | def __init__(self): 44 | self.id = None 45 | self.market_amount = None 46 | self.market_amount_remaining = None 47 | self.created_at = None 48 | self.price = None 49 | self.order_type = None 50 | self.market_id = None 51 | self.open = None 52 | self.trades = None 53 | 54 | 55 | class PairOrders: 56 | def __init__(self): 57 | self.api = api.get( 58 | f"https://api.qtrade.io/v1/user/market/{conf.pair}" 59 | ).json() 60 | 61 | self.base_balance = self.api["data"]["base_balance"] 62 | self.closed_orders = self.api["data"]["closed_orders"] 63 | self.market_balance = self.api["data"]["market_balance"] 64 | self.open_orders = api.auth_native.orders(open=True) 65 | 66 | 67 | class PairMarket: 68 | def __init__(self, configuration): 69 | self.ask = float(conf.market_api["data"]["ask"]) 70 | self.bid = float(conf.market_api["data"]["bid"]) 71 | self.spread = float(abs(self.ask - self.bid)) 72 | 73 | self.day_avg_price = float(conf.market_api["data"]["day_avg_price"]) 74 | self.day_change = float(conf.market_api["data"]["day_change"]) 75 | self.day_high = float(conf.market_api["data"]["day_high"]) 76 | self.day_low = float(conf.market_api["data"]["day_low"]) 77 | self.day_open = float(conf.market_api["data"]["day_open"]) 78 | self.day_volume_base = float(conf.market_api["data"]["day_volume_base"]) 79 | self.day_volume_market = float(conf.market_api["data"]["day_volume_market"]) 80 | self.id = int(conf.market_api["data"]["id"]) 81 | self.id_hr = conf.market_api["data"]["id_hr"] 82 | self.last_price = float(conf.market_api["data"]["last"]) 83 | self.day_spread = float(abs(self.day_high - self.day_low)) 84 | self.spread_pct = 100 - part_percentage(self.bid, self.ask) 85 | 86 | 87 | def pick_currency(balances_dict, currency_name): 88 | 89 | for entry in balances_dict["data"]["balances"]: 90 | if entry["currency"] == currency_name: 91 | return Balance(entry) 92 | 93 | else: 94 | entry = {"currency": currency_name, "balance": 0} 95 | return Balance(entry) 96 | 97 | 98 | class Balance: 99 | def __init__(self, balance_dict): 100 | self.name = balance_dict["currency"] 101 | self.balance = float(balance_dict["balance"]) 102 | 103 | 104 | class Config: 105 | def __init__(self, **kwargs): 106 | self.orders_placed = [] 107 | self.name = kwargs["name"] 108 | self.pair = f"{kwargs['name']}_BTC" 109 | if DEMO: 110 | self.sell_amount = 0 111 | else: 112 | self.sell_amount = float(kwargs["sell_amount"]) 113 | self.buy_amount = float(kwargs["buy_amount"]) 114 | self.buy_longevity = int(kwargs["buy_longevity"]) 115 | self.sell_longevity = int(kwargs["sell_longevity"]) 116 | self.spread_pct_min = float(kwargs["spread_pct_min"]) 117 | self.market_api = None 118 | self.refresh_api() 119 | self.market_id = self.market_api["data"]["id"] 120 | self.price_adjustment = float(kwargs["price_adjustment"]) 121 | log.warning("market_api", self.market_api) 122 | self.max_buy_price = float(kwargs["max_buy_price"]) 123 | self.min_sell_price = float(kwargs["min_sell_price"]) 124 | self.last_refreshed = None 125 | self.max_stash = float(kwargs["max_stash"]) 126 | self.min_stash = float(kwargs["min_stash"]) 127 | self.random_size = float(kwargs["random_size"]) 128 | self.end_pause = float(kwargs["end_pause"]) 129 | 130 | def count_orders(self): 131 | self.orders_count = len(self.orders_placed) 132 | return self.orders_count 133 | 134 | def refresh_api(self): 135 | self.last_refreshed = time.time() 136 | self.market_api = api.get(f"https://api.qtrade.io/v1/ticker/{self.pair}").json() 137 | 138 | 139 | def age(timestamp): 140 | timestamp_ISO_8601 = parser.isoparse(timestamp) 141 | epoch_ts = datetime.timestamp(timestamp_ISO_8601) 142 | return int(time.time() - epoch_ts) 143 | 144 | 145 | def buy(conf, pair_market): 146 | # place a buy order 147 | log.warning("Checking to place a buy order") 148 | if pair_market.bid >= conf.max_buy_price: 149 | log.warning("Market price too high to buy now") 150 | elif conf.buy_amount <= 0: 151 | log.warning(f"Not configured to buy (buy set to {conf.buy_amount})") 152 | 153 | else: 154 | btc = pick_currency(balances, "BTC") 155 | altcoin = pick_currency(balances, conf.name) 156 | random_value = randomize(conf.random_size) 157 | buy_value = conf.buy_amount + random_value 158 | final_price = pair_market.bid + conf.price_adjustment 159 | 160 | if altcoin.balance >= conf.max_stash: 161 | log.warning("Maximum stash reached, will not create new buy orders") 162 | 163 | # log.warning(balance["balance"]) 164 | 165 | elif ( 166 | btc.balance >= buy_value * final_price 167 | ): # if one can afford to buy, sometimes fails because we can't refresh api on every order 168 | 169 | # discount = percentage(trade_price_percentage, pair_market.bid) 170 | req = { 171 | "amount": "%.4f" % buy_value, 172 | "market_id": conf.market_id, 173 | "price": "%.8f" % final_price, 174 | } 175 | 176 | result = api.post( 177 | "https://api.qtrade.io/v1/user/buy_limit", json=req 178 | ).json() 179 | log.warning(result) 180 | order_id = int(result["data"]["order"]["id"]) 181 | log.warning(f"Placed buy order {order_id}") 182 | conf.orders_placed.append({"id": order_id, "order_type": "buy"}) 183 | else: 184 | log.warning( 185 | f"Insufficient balance ({'%.8f' % btc.balance}) for {conf.name} ({conf.buy_amount} units)" 186 | ) 187 | 188 | # place a buy order 189 | 190 | 191 | def randomize(random_size_value): 192 | randomized = float( 193 | "%.4f" % random.uniform((-random_size_value), (random_size_value)) 194 | ) 195 | return randomized 196 | 197 | 198 | def sell(conf, pair_market): 199 | log.warning("Checking to place a sell order") 200 | # place a sell order 201 | if pair_market.ask <= conf.min_sell_price: 202 | log.warning("Market price too low to sell now") 203 | 204 | elif conf.sell_amount <= 0: 205 | log.warning(f"Not configured to sell (sell set to {conf.sell_amount})") 206 | 207 | else: 208 | altcoin = pick_currency(balances, conf.name) 209 | random_value = randomize(conf.random_size) 210 | sell_value = conf.sell_amount + random_value 211 | final_price = pair_market.ask - conf.price_adjustment 212 | 213 | if altcoin.balance <= conf.min_stash: 214 | log.warning("Minimum stash reached, will not create new sell orders") 215 | 216 | elif altcoin.balance >= sell_value: 217 | 218 | # sell order 219 | req = { 220 | "amount": "%.4f" % sell_value, 221 | "market_id": conf.market_id, 222 | "price": "%.8f" % final_price, 223 | } 224 | result = api.post( 225 | "https://api.qtrade.io/v1/user/sell_limit", json=req 226 | ).json() 227 | log.warning(result) 228 | order_id = result["data"]["order"]["id"] 229 | log.warning(f"Placed sell order {order_id}") 230 | conf.orders_placed.append({"id": order_id, "order_type": "sell"}) 231 | else: 232 | log.warning( 233 | f"Insufficient balance ({'%.8f' % altcoin.balance}) for {conf.name} ({conf.buy_amount} units)" 234 | ) 235 | 236 | # place a sell order 237 | 238 | 239 | def pick_longevity_from_type(order_type, conf): 240 | if order_type == "buy_limit": 241 | return conf.buy_longevity 242 | elif order_type == "sell_limit": 243 | return conf.sell_longevity 244 | else: 245 | return None 246 | 247 | 248 | def loop_pair_orders(conf, pair_orders): 249 | # go through orders 250 | for order in pair_orders.open_orders: 251 | if order["market_id"] == conf.market_id: 252 | # log.warning(order["created_at"]) 253 | order_id = int(order["id"]) 254 | order_type = order["order_type"] 255 | age_of_order = age(order["created_at"]) 256 | 257 | longevity = pick_longevity_from_type(order_type, conf) 258 | if age_of_order > longevity: 259 | log.warning( 260 | f"Removing old {order_type} order {order_id}, ({age_of_order}/{longevity}) seconds old" 261 | ) 262 | 263 | req = {"id": order_id} 264 | result = api.post( 265 | "https://api.qtrade.io/v1/user/cancel_order", json=dict(req) 266 | ) 267 | log.warning(result) 268 | 269 | for entry in conf.orders_placed: 270 | if entry["id"] == order_id: 271 | conf.orders_placed.remove(entry) 272 | else: 273 | log.warning( 274 | f"{order_type} order {order_id} retained, {age_of_order}/{longevity} seconds old" 275 | ) 276 | # go through orders 277 | 278 | log.warning(f"Total {conf.name} orders: {conf.orders_placed}") 279 | log.warning(f"Total number of {conf.name} orders: {conf.count_orders()}") 280 | 281 | 282 | def market_stats(conf, pair_market): 283 | log.warning(f"base_balance {pair_orders.base_balance}") 284 | log.warning(f"closed_orders {pair_orders.closed_orders[:10]}") 285 | log.warning(f"market_balance {pair_orders.market_balance}") 286 | log.warning(f"open_orders {pair_orders.open_orders}") 287 | 288 | log.warning( 289 | f"api last refresh: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(conf.last_refreshed))}" 290 | ) 291 | log.warning(f"spread: {'%.8f' % pair_market.spread}") 292 | log.warning(f"ask: {'%.8f' % pair_market.ask}") 293 | log.warning(f"bid: {'%.8f' % pair_market.bid}") 294 | log.warning(f"day_avg_price: {'%.8f' % pair_market.day_avg_price}") 295 | log.warning(f"day_change: {'%.8f' % pair_market.day_change}") 296 | log.warning(f"day_high: {'%.8f' % pair_market.day_high}") 297 | log.warning(f"day_low: {'%.8f' % pair_market.day_low}") 298 | log.warning(f"day_open: {'%.8f' % pair_market.day_open}") 299 | log.warning(f"day_volume_base: {'%.8f' % pair_market.day_volume_base}") 300 | log.warning(f"day_volume_market: {'%.8f' % pair_market.day_volume_market}") 301 | log.warning(f"id: {pair_market.id}") 302 | log.warning(f"id_hr: {pair_market.id_hr}") 303 | log.warning(f"last_price: {'%.8f' % pair_market.last_price}") 304 | log.warning(f"day_spread: {'%.8f' % pair_market.day_spread}") 305 | log.warning(f"spread_pct: {'%.8f' % pair_market.spread_pct}") 306 | 307 | 308 | if __name__ == "__main__": 309 | 310 | # Create a session object to make repeated API calls easy! 311 | api = requests.Session() 312 | # Create an authenticator with your API key 313 | 314 | api.auth_native = QtradeAPI( 315 | "https://api.qtrade.io", key=load_credentials() 316 | ) # use in the future 317 | 318 | api.auth = QtradeAuth(load_credentials()) 319 | 320 | # load currencies 321 | active_currencies = [] 322 | with open("config.json") as confile: 323 | confile_contents = json.loads(confile.read()) 324 | for currency in confile_contents: 325 | log.warning(f"Loaded {currency}") 326 | 327 | active_currencies.append( 328 | Config( 329 | name=currency["name"], 330 | sell_amount=currency["sell_amount"], 331 | buy_amount=currency["buy_amount"], 332 | buy_longevity=currency["buy_longevity"], 333 | sell_longevity=currency["sell_longevity"], 334 | spread_pct_min=currency["spread_pct_min"], 335 | price_adjustment=float(currency["price_adjustment"]), 336 | max_buy_price=currency["max_buy_price"], 337 | min_sell_price=currency["min_sell_price"], 338 | max_stash=currency["max_stash"], 339 | min_stash=currency["min_stash"], 340 | random_size=currency["random_size"], 341 | end_pause=currency["end_pause"], 342 | ) 343 | ) 344 | 345 | # load currencies 346 | 347 | while True: 348 | try: 349 | me = api.get("https://api.qtrade.io/v1/user/me").json() 350 | log.warning(me) 351 | 352 | for conf in active_currencies: 353 | if DEMO and conf.name not in DEMO_MARKETS: 354 | log.warning("Demo mode active, skipping market") 355 | break 356 | 357 | log.warning(f"Working on {conf.pair}") 358 | conf.refresh_api() 359 | # Make a call to API 360 | # move data to object 361 | pair_market = PairMarket(conf) 362 | 363 | pair_orders = PairOrders() 364 | 365 | market_stats(conf, pair_market) 366 | 367 | loop_pair_orders(conf, pair_orders) # pair conf is different 368 | 369 | if pair_market.spread_pct < conf.spread_pct_min: 370 | log.warning( 371 | f"No new orders, spread {pair_market.spread_pct} too small" 372 | ) 373 | 374 | else: 375 | 376 | balances = api.get("https://api.qtrade.io/v1/user/balances").json() 377 | log.warning(balances) 378 | 379 | sell(conf, pair_market) 380 | buy(conf, pair_market) 381 | 382 | log.warning(f"Taking a break for {conf.end_pause} seconds") 383 | time.sleep(conf.end_pause) 384 | 385 | except Exception as e: 386 | log.warning(f"Exception {e}") 387 | if DEBUG: 388 | raise 389 | 390 | log.warning("Loop ended, waiting for 60 seconds") 391 | time.sleep(60) 392 | --------------------------------------------------------------------------------