├── LICENSE ├── MortyBot.py ├── README.md └── sample-bot.cfg /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 PyWaves 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 | -------------------------------------------------------------------------------- /MortyBot.py: -------------------------------------------------------------------------------- 1 | import pywaves as pw 2 | import datetime 3 | import time 4 | import os 5 | import sys 6 | import random 7 | try: 8 | import configparser 9 | except ImportError: 10 | import ConfigParser as configparser 11 | 12 | class MortyBot: 13 | def __init__(self): 14 | self.log_file = "bot.log" 15 | self.node = "https://privatenode2.blackturtle.eu" 16 | self.chain = "turtlenetwork" 17 | self.matcher = "https://privatematcher.blackturtle.eu" 18 | self.datafeed = "https://bot.blackturtle.eu" 19 | self.order_fee = int(0.04 * 10 ** 8) 20 | self.order_lifetime = 1 * 86400 # 1 days 21 | self.private_key = "" 22 | self.amount_asset_id = "" 23 | self.amount_asset = pw.WAVES 24 | self.price_asset_id = "4LHHvYGNKJUg5hj65aGD5vgScvCBmLpdRFtjokvCjSL8" # BTC 25 | self.price_asset = pw.Asset(self.price_asset_id) 26 | self.price_step = 0.05 27 | self.min_amount = 10000 28 | self.seconds_to_sleep = 5 29 | self.strategy = "grid" 30 | self.grid = ["-"] 31 | self.grid_levels = 20 32 | self.grid_tranche = 10000000000 33 | self.grid_base = "LAST" 34 | self.grid_flexibility = 20 35 | self.grid_type = "SYMMETRIC" 36 | self.grid_basePrice = 0 37 | self.can_buy = "" 38 | self.can_sell = "" 39 | self.uptr_profitmargin = 0.05 40 | self.uptr_stoploss = 0.01 41 | self.uptr_stoploss_current = 0 42 | self.buy_orderid = "" 43 | self.sell_orderid = "" 44 | self.filled_type = "" 45 | self.filled_price = "" 46 | 47 | def log(self, msg): 48 | timestamp = datetime.datetime.now().strftime("%b %d %Y %H:%M:%S") 49 | s = "[{0}]:{1}".format(timestamp, msg) 50 | print(s) 51 | try: 52 | f = open(self.log_file, "a") 53 | f.write(s + "\n") 54 | f.close() 55 | except OSError: 56 | pass 57 | 58 | def read_config(self, cfg_file): 59 | if not os.path.isfile(cfg_file): 60 | self.log("Missing config file") 61 | self.log("Exiting.") 62 | exit(1) 63 | 64 | try: 65 | self.log("Reading config file '{0}'".format(cfg_file)) 66 | config = configparser.RawConfigParser() 67 | config.read(cfg_file) 68 | self.node = config.get('network', 'node') 69 | self.chain = config.get('network', 'network') 70 | self.matcher = config.get('network', 'matcher') 71 | self.datafeed = config.get('network', 'datafeed') 72 | self.order_fee = config.getint('network', 'order_fee') 73 | 74 | self.order_lifetime = config.getint('main', 'order_lifetime') 75 | self.strategy = config.get('main', 'strategy').lower() 76 | self.seconds_to_sleep = config.getint('main', 'sleeptimer') 77 | 78 | self.private_key = config.get('account', 'private_key') 79 | 80 | self.amount_asset_id = config.get('market', 'amount_asset') 81 | self.amount_asset = pw.Asset(self.amount_asset_id) 82 | 83 | self.price_asset_id = config.get('market', 'price_asset') 84 | self.price_asset = pw.Asset(self.price_asset_id) 85 | 86 | self.price_step = config.getfloat('grid', 'interval') 87 | self.grid_tranche = config.getint('grid', 'tranche_size') 88 | self.grid_flexibility = config.getint('grid', 'flexibility') 89 | self.grid_levels = config.getint('grid', 'grid_levels') 90 | self.grid_base = config.get('grid', 'base').upper() 91 | self.grid_type = config.get('grid', 'type').upper() 92 | 93 | self.uptr_profitmargin = config.getfloat('uptrend', 'profitmargin') 94 | self.uptr_stoploss = config.getfloat('uptrend', 'stoploss') 95 | 96 | self.log_file = config.get('logging', 'logfile') 97 | except OSError: 98 | self.log("Error reading config file") 99 | self.log("Exiting.") 100 | exit(1) 101 | 102 | def grid_price(self, level): 103 | return int(self.grid_basePrice * (1 + self.price_step) ** (level - self.grid_levels / 2)) 104 | 105 | def give_price(price): 106 | newprice = int(price / 100) * 100 107 | newprice = round(newprice / 10 ** (PAIR2.asset2.decimals + (PAIR2.asset2.decimals - PAIR2.asset1.decimals)), 8) 108 | newprice = float(str(newprice)) 109 | 110 | mewprice = (round(price / pow(10, PAIR2.asset2.decimals - PAIR2.asset1.decimals)) * pow(10, PAIR2.asset2.decimals - PAIR2.asset1.decimals)) 111 | mewprice = mewprice / pow(10, 8 + PAIR2.asset2.decimals - PAIR2.asset1.decimals) 112 | 113 | return newprice 114 | 115 | def grid_place_order(bot, order_type, level): 116 | if 0 <= level < bot.grid_levels and (bot.grid[level] == "" or bot.grid[level] == "-"): 117 | price = bot.grid_price(level) 118 | showprice = price / pow(10, PAIR2.asset2.decimals + (PAIR2.asset2.decimals - PAIR2.asset1.decimals)) 119 | price = (round(price / pow(10, PAIR2.asset2.decimals - PAIR2.asset1.decimals)) * pow(10, PAIR2.asset2.decimals - PAIR2.asset1.decimals)) 120 | price = price / pow(10, 8 + PAIR2.asset2.decimals - PAIR2.asset1.decimals) 121 | 122 | try: 123 | balance_amount, balance_price = my_address.tradableBalance(PAIR) 124 | tranche_size = bot.grid_tranche 125 | tranche_size = int(tranche_size * (1 - (bot.grid_flexibility / float(200)) + (random.random() * bot.grid_flexibility / float(100)))) 126 | 127 | if order_type == "buy": 128 | if (bot.chain == "turtlenetwork" and bot.price_asset_id == "TN") or (bot.chain != "turtlenetwork" and bot.price_asset_id == "WAVES"): 129 | required_amount = ((tranche_size * price) + bot.order_fee) 130 | else: 131 | bid_amount = (tranche_size * price) * pow(10, PAIR2.asset2.decimals) 132 | required_amount = (tranche_size * price) 133 | 134 | if balance_price >= required_amount: 135 | bot.can_buy = "yes" 136 | 137 | o = my_address.buy(PAIR2, tranche_size, price, maxLifetime=bot.order_lifetime, matcherFee=bot.order_fee) 138 | try: 139 | id = o.orderId 140 | bot.log(">> [{0}] {1} order price: {2}, amount:{3}".format(level, order_type.upper(), showprice, float(tranche_size) / pow(10, PAIR2.asset2.decimals + (PAIR2.asset2.decimals - PAIR2.asset1.decimals)))) 141 | except: 142 | id = "" 143 | 144 | else: 145 | if bot.can_buy != "no": 146 | bot.log(">> Insufficient funds for BUY order") 147 | 148 | bot.can_buy = "no" 149 | id = "" 150 | elif order_type == "sell": 151 | if (bot.chain == "turtlenetwork" and bot.price_asset_id == "TN") or (bot.chain != "turtlenetwork" and bot.price_asset_id == "WAVES"): 152 | required_amount = tranche_size 153 | else: 154 | required_amount = tranche_size - bot.order_fee 155 | 156 | balance_amount, balance_price = my_address.tradableBalance(PAIR) 157 | if balance_amount >= required_amount: 158 | bot.can_sell = "yes" 159 | 160 | o = my_address.sell(PAIR2, tranche_size, price, maxLifetime=bot.order_lifetime, matcherFee=bot.order_fee) 161 | try: 162 | id = o.orderId 163 | bot.log(">> [{0}] {1} order price: {2}, amount:{3}".format(level, order_type.upper(), showprice, float(tranche_size) / pow(10, PAIR2.asset2.decimals + (PAIR2.asset2.decimals - PAIR2.asset1.decimals)))) 164 | except: 165 | id = "" 166 | else: 167 | if bot.can_sell != "no": 168 | bot.log(">> Insufficient funds for SELL order") 169 | 170 | bot.can_sell = "no" 171 | id = "" 172 | 173 | except Exception as e: 174 | print(str(e)) 175 | id = "" 176 | 177 | bot.grid[level] = id 178 | 179 | def get_last_price(): 180 | try: 181 | last_trade_price = int(round(float(float(PAIR.trades(1)[0]['price']) * pow(10, 8 + PAIR2.asset2.decimals - PAIR2.asset1.decimals)))) 182 | except Exception as e: 183 | print("Exception ") 184 | print(str(e)) 185 | last_trade_price = 0 186 | return last_trade_price 187 | 188 | def check_order(bot, orderid): 189 | #check if order has been filled 190 | # attempt to retrieve order history from matcher 191 | try: 192 | history = my_address.getOrderHistory(PAIR) 193 | except: 194 | history = [] 195 | 196 | if history: 197 | order = [item for item in history if item['id'] == orderid] 198 | status = order[0].get("status") if order else "" 199 | if status == "Filled": 200 | my_address.deleteOrderHistory(PAIR) 201 | bot.filled_price = order[0].get("price") 202 | bot.filled_type = order[0].get("type") 203 | 204 | return status 205 | else: 206 | return "no_history" 207 | 208 | def initialize_grid(bot): 209 | # initialize grid 210 | try: 211 | if bot.grid_base.isdigit(): 212 | bot.grid_basePrice = int(bot.grid_base) 213 | elif bot.grid_base == "LAST": 214 | bot.grid_basePrice = get_last_price() 215 | elif bot.grid_base == "BID": 216 | bot.grid_basePrice = PAIR.orderbook()['bids'][0]['price'] 217 | elif bot.grid_base == "ASK": 218 | bot.grid_basePrice = PAIR.orderbook()['asks'][0]['price'] 219 | bot.log(">> GRID_BASE: "+str(bot.grid_basePrice)) 220 | except: 221 | bot.grid_basePrice = 0 222 | if bot.grid_basePrice == 0: 223 | bot.log("Invalid BASE price") 224 | bot.log("Exiting.") 225 | exit(1) 226 | 227 | bot.log(">> Grid initialisation [base price : %.*f]" % (PAIR2.asset2.decimals, float(bot.grid_basePrice) / pow(10, PAIR2.asset2.decimals + (PAIR2.asset2.decimals - PAIR2.asset1.decimals)))) 228 | 229 | last_level = int(bot.grid_levels / 2) 230 | 231 | if bot.grid_type == "SYMMETRIC" or bot.grid_type == "BIDS": 232 | for n in range(last_level - 1, -1, -1): 233 | grid_place_order(bot, "buy", n) 234 | if bot.grid_type == "SYMMETRIC" or bot.grid_type == "ASKS": 235 | for n in range(last_level + 1, bot.grid_levels): 236 | grid_place_order(bot, "sell", n) 237 | 238 | def check_moving_grid(bot, n): 239 | #check if we need to reinitialize the grid 240 | if bot.strategy == "moving_grid": 241 | if n == bot.grid_levels - 1: 242 | return True 243 | else: 244 | return False 245 | 246 | 247 | def go_grid(bot): 248 | #grid strategy main procedure 249 | # delete order history on the specified pair 250 | bot.log(">> Deleting order history...") 251 | my_address.deleteOrderHistory(PAIR) 252 | bot.log("") 253 | 254 | # grid list with GRID_LEVELS items. item n is the ID of the order placed at the price calculated with this formula 255 | bot.grid = ["-"] * bot.grid_levels 256 | 257 | # initialize grid 258 | initialize_grid(bot) 259 | 260 | last_level = int(bot.grid_levels / 2) 261 | 262 | check = 0 263 | # loop forever 264 | while True: 265 | check = check + 1 266 | if check == 360: #every 35 minutes 267 | bot.log(">> No orders hit - bot is still running") 268 | check = 0 269 | 270 | # loop through all grid levels 271 | # first all ask levels from the lowest ask to the highest -> range(grid.index("") + 1, len(grid)) 272 | # then all bid levels from the highest to the lowest -> range(grid.index("") - 1, -1, -1) 273 | for n in list(range(last_level + 1, len(bot.grid))) + list(range(last_level - 1, -1, -1)): 274 | 275 | # find the order with id == grid[n] in the history list 276 | status = check_order(bot, bot.grid[n]) 277 | if status == "Filled": 278 | last_price = get_last_price() 279 | bot.grid[n] = "" 280 | last_level = n 281 | check = 0 282 | 283 | bot.log("## [%03d] %s%-4s Filled %18.*f%s" % (n, "", bot.filled_type.upper(), PAIR2.asset2.decimals, float(bot.filled_price) / 10 ** (PAIR2.asset2.decimals + (PAIR2.asset2.decimals - PAIR2.asset1.decimals)), "")) 284 | 285 | if bot.filled_type == "buy": 286 | if bot.filled_price >= last_price: 287 | grid_place_order(bot, "sell", n + 1) 288 | else: 289 | grid_place_order(bot, "buy", n) 290 | elif bot.filled_type == "sell": 291 | if bot.filled_price <= last_price: 292 | #check if we need to move the grid 293 | if check_moving_grid(bot, n): 294 | initialize_grid(bot) 295 | else: 296 | grid_place_order(bot, "buy", n - 1) 297 | else: 298 | grid_place_order(bot, "sell", n) 299 | # attempt to place again orders for empty grid levels or cancelled orders 300 | elif (status == "" or status == "Cancelled") and bot.grid[n] != "-": 301 | bot.grid[n] = "" 302 | if n > last_level: 303 | grid_place_order(bot, "sell", n) 304 | elif n < last_level: 305 | grid_place_order(bot, "buy", n) 306 | 307 | time.sleep(bot.seconds_to_sleep) 308 | 309 | def go_scalp(bot): 310 | #scalp strategy main procedure 311 | while True: 312 | balance_amount, balance_price = my_address.tradableBalance(PAIR) 313 | order_book = PAIR.orderbook() 314 | best_bid = order_book["bids"][0]["price"] 315 | best_ask = order_book["asks"][0]["price"] 316 | spread_mean_price = ((best_bid + best_ask) // 2) #* 10 ** (PAIR2.asset2.decimals - PAIR2.asset1.decimals) 317 | bid_price = spread_mean_price * (1 - bot.price_step) 318 | ask_price = spread_mean_price * (1 + bot.price_step) 319 | if (bot.chain == "turtlenetwork" and bot.price_asset_id == "TN") or (bot.chain != "turtlenetwork" and bot.price_asset_id == "WAVES"): 320 | bid_amount = int(((balance_price - bot.order_fee) / bid_price) * pow(10, PAIR2.asset2.decimals)) 321 | else: 322 | bid_amount = int((balance_price / bid_price) * pow(10, PAIR2.asset2.decimals)) 323 | 324 | if bid_amount >= bot.min_amount: 325 | bot.log("Best_bid: {0}, best_ask: {1}, spread mean price: {2}".format(best_bid, best_ask, spread_mean_price)) 326 | price = give_price(bid_price) 327 | bot.log("Post buy order with price: {0}, amount:{1}".format(price, float(bid_amount) / pow(10, PAIR2.asset2.decimals + (PAIR2.asset2.decimals - PAIR2.asset1.decimals)))) 328 | my_address.buy(assetPair=PAIR2, amount=bid_amount, price=price, matcherFee=bot.order_fee, maxLifetime=bot.order_lifetime) 329 | 330 | balance_amount, balance_price = my_address.tradableBalance(PAIR) 331 | if (bot.chain == "turtlenetwork" and bot.price_asset_id == "TN") or (bot.chain != "turtlenetwork" and bot.price_asset_id == "WAVES"): 332 | ask_amount = balance_amount 333 | else: 334 | ask_amount = balance_amount - bot.order_fee #int(((balance_amount - bot.order_fee) / ask_price) * pow(10, PAIR2.asset2.decimals)) 335 | 336 | if ask_amount >= bot.min_amount: 337 | bot.log("Best_bid: {0}, best_ask: {1}, spread mean price: {2}".format(best_bid, best_ask, spread_mean_price)) 338 | price = give_price(ask_price) 339 | bot.log("Post sell order with price: {0}, ask amount: {1}".format(price, float(ask_amount) / pow(10, PAIR2.asset2.decimals + (PAIR2.asset2.decimals - PAIR2.asset1.decimals)))) 340 | my_address.sell(assetPair=PAIR2, amount=ask_amount, price=price, matcherFee=bot.order_fee, maxLifetime=bot.order_lifetime) 341 | 342 | #bot.log("Sleep {0} seconds...".format(bot.seconds_to_sleep)) 343 | time.sleep(bot.seconds_to_sleep) 344 | 345 | def set_stoploss(bot, price): 346 | #set stoploss price 347 | bot.uptr_stoploss_current = price 348 | bot.log(">> Stoploss set at: {0}".format(give_price(bot.uptr_stoploss_current))) 349 | 350 | def fill_stoploss(bot): 351 | #stoploss hit, liquidate asset 352 | balance_amount, balance_price = my_address.tradableBalance(PAIR) 353 | order_book = PAIR.orderbook() 354 | best_bid = order_book["bids"][0]["price"] 355 | price = give_price(best_bid) 356 | 357 | #get current amount 358 | if (bot.chain == "turtlenetwork" and bot.price_asset_id == "TN") or (bot.chain != "turtlenetwork" and bot.price_asset_id == "WAVES"): 359 | ask_amount = int(balance_amount) 360 | else: 361 | ask_amount = int(((balance_amount - bot.order_fee) / price) * pow(10, PAIR2.asset2.decimals)) 362 | 363 | #check if there's anything to sell 364 | if ask_amount > 1000: 365 | bot.log(">> Stoploss hit: Post sell order with price: {0}, amount: {1}".format(price, ask_amount)) 366 | 367 | #sell until completely liquidated 368 | while ask_amount > 1000: 369 | order_book = PAIR.orderbook() 370 | best_bid = order_book["bids"][0]["price"] 371 | price = give_price(best_bid) 372 | 373 | #sell for current buy price 374 | my_address.sell(assetPair=PAIR2, amount=ask_amount, price=price, matcherFee=bot.order_fee, maxLifetime=bot.order_lifetime) 375 | 376 | time.sleep(bot.seconds_to_sleep) 377 | 378 | #check if everything is sold 379 | balance_amount, balance_price = my_address.tradableBalance(PAIR) 380 | if (bot.chain == "turtlenetwork" and bot.price_asset_id == "TN") or (bot.chain != "turtlenetwork" and bot.price_asset_id == "WAVES"): 381 | ask_amount = int(balance_amount) 382 | else: 383 | ask_amount = int(((balance_amount - bot.order_fee) / price) * pow(10, PAIR2.asset2.decimals)) 384 | 385 | bot.log(">> Stoploss filled: all assets liquidated") 386 | exit(1) 387 | 388 | def go_uptrend(bot): 389 | #uptrend strategy main procedure 390 | #first initialization 391 | balance_amount, balance_price = my_address.tradableBalance(PAIR) 392 | order_book = PAIR.orderbook() 393 | #get current bids 394 | best_bid = order_book["bids"][0]["price"] 395 | best_ask = order_book["asks"][0]["price"] 396 | spread_mean_price = ((best_bid + best_ask) // 2) 397 | bid_price = best_ask 398 | #calculate sell price 399 | ask_price = spread_mean_price * (1 + bot.uptr_profitmargin) 400 | 401 | #set stoploss price 402 | if bot.uptr_stoploss_current == 0: 403 | set_stoploss(bot, bid_price * (1 - bot.uptr_stoploss)) 404 | 405 | #calculate max buy amount 406 | if (bot.chain == "turtlenetwork" and bot.price_asset_id == "TN") or (bot.chain != "turtlenetwork" and bot.price_asset_id == "WAVES"): 407 | bid_amount = int(((balance_price - bot.order_fee) / bid_price) * pow(10, PAIR2.asset2.decimals)) 408 | else: 409 | bid_amount = int((balance_price / bid_price) * pow(10, PAIR2.asset2.decimals)) 410 | 411 | #buy for current sell price 412 | if bid_amount >= bot.min_amount: 413 | price = give_price(bid_price) 414 | bot.log(">> Post buy order with price: {0}, amount: {1}".format(price, float(bid_amount) / pow(10, PAIR2.asset2.decimals + (PAIR2.asset2.decimals - PAIR2.asset1.decimals)))) 415 | o = my_address.buy(assetPair=PAIR2, amount=bid_amount, price=price, matcherFee=bot.order_fee, maxLifetime=bot.order_lifetime) 416 | try: 417 | bot.buy_orderid = o.orderId 418 | except: 419 | bot.buy_orderid = "" 420 | 421 | #calculate max sell amount 422 | balance_amount, balance_price = my_address.tradableBalance(PAIR) 423 | if (bot.chain == "turtlenetwork" and bot.price_asset_id == "TN") or (bot.chain != "turtlenetwork" and bot.price_asset_id == "WAVES"): 424 | ask_amount = balance_amount 425 | else: 426 | ask_amount = balance_amount #int(((balance_amount - bot.order_fee) / ask_price) * pow(10, PAIR2.asset2.decimals)) 427 | 428 | #post sell order 429 | if ask_amount >= bot.min_amount: 430 | price = give_price(ask_price) 431 | bot.log(">> Post sell order with price: {0}, amount: {1}".format(price, float(ask_amount) / pow(10, PAIR2.asset2.decimals + (PAIR2.asset2.decimals - PAIR2.asset1.decimals)))) 432 | o = my_address.sell(assetPair=PAIR2, amount=ask_amount, price=price, matcherFee=bot.order_fee, maxLifetime=bot.order_lifetime) 433 | try: 434 | bot.sell_orderid = o.orderId 435 | except: 436 | bot.sell_orderid = "" 437 | 438 | #loop forever 439 | while True: 440 | balance_amount, balance_price = my_address.tradableBalance(PAIR) 441 | order_book = PAIR.orderbook() 442 | #get current bids 443 | best_bid = order_book["bids"][0]["price"] 444 | best_ask = order_book["asks"][0]["price"] 445 | spread_mean_price = ((best_bid + best_ask) // 2) 446 | bid_price = best_ask 447 | #calculate sell price 448 | ask_price = spread_mean_price * (1 + bot.uptr_profitmargin) 449 | 450 | #check stoploss 451 | if best_bid <= bot.uptr_stoploss_current: 452 | my_address.cancelOpenOrders(PAIR) 453 | balance_amount, balance_price = my_address.tradableBalance(PAIR) 454 | 455 | fill_stoploss(bot) 456 | 457 | status = "" 458 | #check buyorder 459 | if bot.buy_orderid != "": 460 | status = check_order(bot, bot.buy_orderid) 461 | 462 | if status == "Filled": 463 | bot.buy_orderid = "" 464 | bot.log("## {0} order Filled at: {1}".format(bot.filled_type.upper(), float(bot.filled_price) / 10 ** (PAIR2.asset2.decimals + (PAIR2.asset2.decimals - PAIR2.asset1.decimals)))) 465 | 466 | #reset stoploss 467 | set_stoploss(bot, bot.filled_price * (1 - bot.uptr_stoploss)) 468 | 469 | #calculate max sell amount 470 | balance_amount, balance_price = my_address.tradableBalance(PAIR) 471 | if (bot.chain == "turtlenetwork" and bot.price_asset_id == "TN") or (bot.chain != "turtlenetwork" and bot.price_asset_id == "WAVES"): 472 | ask_amount = balance_amount 473 | else: 474 | ask_amount = balance_amount #int(((balance_amount - bot.order_fee) / ask_price) * pow(10, PAIR2.asset2.decimals)) 475 | 476 | #post sell order 477 | #if ask_amount >= bot.min_amount: 478 | price = give_price(ask_price) 479 | bot.log(">> Post sell order with price: {0}, amount: {1}".format(price, float(ask_amount) / pow(10, PAIR2.asset2.decimals + (PAIR2.asset2.decimals - PAIR2.asset1.decimals)))) 480 | o = my_address.sell(assetPair=PAIR2, amount=ask_amount, price=price, matcherFee=bot.order_fee, maxLifetime=bot.order_lifetime) 481 | try: 482 | bot.sell_orderid = o.orderId 483 | except: 484 | bot.sell_orderid = "" 485 | 486 | #check sellorder 487 | if bot.sell_orderid != "": 488 | status = check_order(bot, bot.sell_orderid) 489 | 490 | if status == "Filled": 491 | bot.sell_orderid = "" 492 | bot.log("## {0} order Filled at: {1}".format(bot.filled_type.upper(), float(bot.filled_price) / 10 ** (PAIR2.asset2.decimals + (PAIR2.asset2.decimals - PAIR2.asset1.decimals)))) 493 | 494 | #reset stoploss 495 | set_stoploss(bot, bid_price * (1 - bot.uptr_stoploss)) 496 | 497 | #calculate max buy amount 498 | if (bot.chain == "turtlenetwork" and bot.price_asset_id == "TN") or (bot.chain != "turtlenetwork" and bot.price_asset_id == "WAVES"): 499 | bid_amount = int(((balance_price - bot.order_fee) / bid_price) * pow(10, PAIR2.asset2.decimals)) 500 | else: 501 | bid_amount = int((balance_price / bid_price) * pow(10, PAIR2.asset2.decimals)) 502 | 503 | #buy for current sell price 504 | #if bid_amount >= bot.min_amount: 505 | price = give_price(bid_price) 506 | bot.log(">> Post buy order with price: {0}, amount: {1}".format(price, float(bid_amount) / pow(10, PAIR2.asset2.decimals + (PAIR2.asset2.decimals - PAIR2.asset1.decimals)))) 507 | o = my_address.buy(assetPair=PAIR2, amount=bid_amount, price=price, matcherFee=bot.order_fee, maxLifetime=bot.order_lifetime) 508 | try: 509 | bot.buy_orderid = o.orderId 510 | except: 511 | bot.buy_orderid = "" 512 | 513 | #bot.log("Sleep {0} seconds...".format(bot.seconds_to_sleep)) 514 | time.sleep(bot.seconds_to_sleep) 515 | 516 | 517 | def main(): 518 | global my_address 519 | global PAIR 520 | global PAIR2 521 | 522 | #initialisation 523 | bot = MortyBot() 524 | 525 | CFG_FILE = "bot.cfg" 526 | 527 | if len(sys.argv) >= 2: 528 | CFG_FILE = sys.argv[1] 529 | 530 | if not os.path.isfile(CFG_FILE): 531 | bot.log("Missing config file") 532 | bot.log("Exiting.") 533 | exit(1) 534 | 535 | bot.read_config(CFG_FILE) 536 | pw.setMatcher(node=bot.matcher) 537 | pw.setDatafeed(wdf=bot.datafeed) 538 | 539 | if bot.chain == "turtlenetwork": 540 | pw.setNode(bot.node, bot.chain, 'L') 541 | PAIR = pw.AssetPair(pw.Asset(bot.amount_asset_id), pw.Asset(bot.price_asset_id)) 542 | PAIR2 = PAIR 543 | if bot.price_asset_id == 'TN': 544 | PAIR2 = pw.AssetPair(pw.Asset(bot.amount_asset_id), pw.Asset('WAVES')) 545 | elif bot.amount_asset_id == 'TN': 546 | PAIR2 = pw.AssetPair(pw.Asset('WAVES'), pw.Asset(bot.price_asset_id)) 547 | else: 548 | pw.setNode(bot.node, bot.chain) 549 | PAIR = pw.AssetPair(pw.Asset(bot.amount_asset_id), pw.Asset(bot.price_asset_id)) 550 | PAIR2 = PAIR 551 | if bot.price_asset_id == 'WAVES': 552 | PAIR2 = pw.AssetPair(pw.Asset(bot.amount_asset_id), pw.WAVES) 553 | elif bot.amount_asset_id == 'WAVES': 554 | PAIR2 = pw.AssetPair(pw.WAVES, pw.Asset(bot.price_asset_id)) 555 | 556 | my_address = pw.Address(privateKey=bot.private_key) 557 | 558 | bot.log("-" * 80) 559 | bot.log(" Address : {0}".format(my_address.address)) 560 | bot.log(" Amount Asset ID : {0} ({1})".format(bot.amount_asset_id, PAIR2.asset1.name)) 561 | bot.log(" Price Asset ID : {0} ({1})".format(bot.price_asset_id, PAIR2.asset2.name)) 562 | bot.log("Strategy selected : {0}".format(bot.strategy.upper())) 563 | bot.log("-" * 80) 564 | bot.log("") 565 | 566 | # cancel all open orders on the specified pair 567 | bot.log(">> Cancelling open orders...") 568 | my_address.cancelOpenOrders(PAIR) 569 | 570 | #grid trading 571 | if bot.strategy.lower() == "grid" or bot.strategy.lower() == "moving_grid": 572 | go_grid(bot) 573 | elif bot.strategy.lower() == "scalp": 574 | go_scalp(bot) 575 | elif bot.strategy.lower() == "uptrend": 576 | bot.seconds_to_sleep = 1 577 | go_uptrend(bot) 578 | 579 | 580 | if __name__ == "__main__": 581 | main() 582 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MortyBot 2 | 3 | MortyBot is a Python bot implementing several strategies. It can work with any assets pair on the WAVES DEX and TurtleNetwork DEX. 4 | 5 | The bot is based on https://github.com/BlackTurtle123/BlackBotTN and https://github.com/wavesplatform/demo-python-trading-bot 6 | 7 | The main reason for creating my own bot and not using the two above was that i was constantly running into errors/problems when trying out different trading pairs. Initially i tried just fixing the errors as they popped up, but in the end i thought it would be easier to create a new bot where i could work out all the kinks and integrate more strategies. 8 | 9 | Currently included strategies are: GRID, SCALP, UPTREND and MOVING_GRID 10 | 11 | ## Grid Trading 12 | Grid trading doesn’t care about which way the market’s going — in fact, as a profitable strategy it works best in ranging markets. The strategy places a ladder of sells at regular intervals above market price, and another ladder of buys beneath it. If a sell is filled, those funds are used to place a buy just beneath that sell. Thus you can think of the grid as a series of pairs of buys/sells stretching up and down the price chart, with either the buy or sell in each pair always active. 13 | 14 | For example, let’s say the last price is 2000 satoshis you’ve got sells laddered up at 2100, 2200, 2300… If the price hits 2100, you immediately use those funds to place a new buy at 2000. If it drops to 2000 again, you buy back the Incent you sold at 2100. If it rises further, you sell at 2200 and open a buy at 2100. Whichever way the price moves, you’re providing depth — buffering the market and smoothing out any peaks and troughs. Additionally, if you open and then close a trade within a tranche (e.g. you sell at 2200, then buy back at 2100) then you make a small profit. 15 | 16 | ## Moving Grid Trading 17 | Moving Grid trading takes the bases of Grid trading, but ads the functionality to recalculate the Grid when the market moves out of the Grid range. Ones the Bot detects that the last filled trade is the last sell order in the grid it will reinitialise the Grid for the current market conditions. This will only work of you haven't set a constant price for base calculations and the grid type is set to SYMMETRIC. 18 | 19 | ## Scalp Trading 20 | Scalping exploits small changes in currency prices: it buys at the mean price minus some step and sells at the mean price plus some step, in order to gain the bid/ask difference. It normally involves establishing and liquidating a position quickly. 21 | 22 | For example, let's trade on TN-BTC pair (TN is an amount asset and BTC is a price_asset). The spread mean price is ```(best_bid + best_ask) / 2```. The price step is ```0.5%``` from mean price. MortyBot will place the buy order at price ```meanprice * (1 - price_step)``` and the amount ```(BTC_balance / bid_price) - order_fee```. The sell order is placed at ```meanprice * (1 + price_step)``` and the amount equal to ```TN_balance - order_fee```. 23 | ### The SCALP strategy will use the total balance in the set wallet for trading the selected pair, so use a seperate trader wallet! 24 | 25 | ## Uptrend Trading 26 | Updrend trading is used when you expect for a pair to start pumping but don't want to monitor it the whole time. The PROFITMARGIN and STOPLOSS values are percentage values that you set. MortyBot will buy the set currency for current market price and place a sell order at current market price + profitmargin. Furthermore it will calculate the stoploss value at current market price - stoploss. If the market goes up and the sell order is hit, it will repeat the process. If the market goes down and the stoploss is hit, MortyBot will liquidate ALL of the set currency at the last available ask (either stoploss price or below). 27 | ### The UPTREND strategy will use the total balance in the set wallet for trading the selected pair, so use a seperate trader wallet! 28 | 29 | ## Getting Started 30 | 31 | Create a new wallet for use with the trader bot and move the funds you want to use over to it. 32 | 33 | MortyBot requires Python 2.7 or 3.x and the following Python packages: 34 | 35 | * PyWaves 36 | * ConfigParser (with Python 2.7) 37 | * configparser (with Python 3.x) 38 | 39 | You can install them with 40 | 41 | ``` 42 | pip install pywaves 43 | pip install ConfigParser (python 2.7) 44 | pip install configparser (python 3.x) 45 | ``` 46 | 47 | You can start MortyBot with this command: 48 | 49 | ``` 50 | python MortyBot.py sample-bot.cfg 51 | ``` 52 | 53 | below you can find a sample configuration file: 54 | ``` 55 | [network] 56 | node = https://privatenode2.blackturtle.eu 57 | network = turtlenetwork 58 | matcher = https://privatematcher.blackturtle.eu 59 | datafeed = https://bot.blackturtle.eu 60 | order_fee = 4000000 61 | 62 | [main] 63 | order_lifetime = 86400 64 | sleeptimer = 5 65 | strategy = grid 66 | 67 | [account] 68 | private_key = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 69 | 70 | [market] 71 | amount_asset = TN 72 | price_asset = 8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS 73 | 74 | [grid] 75 | interval = 0.005 76 | tranche_size = 200000000 77 | grid_levels = 20 78 | base = last 79 | type = symmetric 80 | 81 | [uptrend] 82 | profitmargin = 0.05 83 | stoploss = 0.03 84 | 85 | [logging] 86 | logfile = bot.log 87 | ``` 88 | 89 | #### network section 90 | ```node``` is the address of the fullnode 91 | 92 | ```matcher``` is the matcher address 93 | 94 | ```order_fee``` is the fee to place buy and sell orders 95 | 96 | #### main section 97 | ```order_fee``` is the fee to place buy and sell orders 98 | 99 | ```sleeptimer``` is the number of seconds the bot waits before rechecking orders 100 | 101 | ```strategy``` is the strategy to use (grid, moving_grid, uptrend or scalp) 102 | 103 | #### account section 104 | ```private_key``` is the private key of the trading account 105 | 106 | #### market section 107 | ```amount_asset``` and ```price_asset``` are the IDs of the traded assets pair 108 | 109 | #### grid section 110 | ```interval``` is the % interval between grid levels 111 | 112 | ```tranche_size``` is the size amount of each buy and sell order 113 | 114 | ```grid_levels``` is the number of grid levels 115 | 116 | ```base``` is the price level around which the grid is setup; it can be LAST, for the last traded price, BID for the current bid price, ASK for the current ask price or a fixed constant price can be specified 117 | 118 | ```flexibility``` amount flexibility in percent, 20% flexibility means that the amount of the order might flucture +/- 10% around the defined tranche_size 119 | 120 | ```type``` the initial grid can be SYMMETRIC, if there are both buy and sell orders; BIDS if the are only buy orders; ASKS if there are only sell orders 121 | 122 | #### uptrend section 123 | ```profitmargin``` is the profit % where your sellorder will be set - marketprice + profitmargin % 124 | 125 | ```stoploss``` is the loss % you are willing to take - marketprice - stoploss % 126 | 127 | #### logging section 128 | ```logfile``` is the file where the log will be written 129 | -------------------------------------------------------------------------------- /sample-bot.cfg: -------------------------------------------------------------------------------- 1 | [network] 2 | node = https://privatenode2.blackturtle.eu 3 | # select the network: testnet or mainnet 4 | network = turtlenetwork 5 | matcher = https://privatematcher.blackturtle.eu 6 | datafeed = https://bot.blackturtle.eu 7 | order_fee = 4000000 8 | 9 | [main] 10 | #time in seconds 11 | order_lifetime = 86400 12 | sleeptimer = 5 13 | #grid, scalp, uptrend or moving_grid 14 | strategy = scalp 15 | 16 | [account] 17 | private_key = 18 | 19 | [market] 20 | amount_asset = 6Mh41byVWPg8JVCfuwG5CAPCh9Q7gnuaAVxjDfVNDmcD 21 | price_asset = TN 22 | 23 | [grid] 24 | #these settings are for grid and moving_grid 25 | interval = 0.005 26 | #tranche_size = 100000000000 lower is smaller steps 27 | tranche_size = 100000000000 28 | grid_levels = 20 29 | # base price calculation: LAST, BID, ASK, nnnnn (fixed constant price) 30 | base = last 31 | # amount flexibility in percent, 20% flexibility means that the amount of the order might flucture +/- 10% around the defined tranche_size 32 | flexibility = 20 33 | # grid type: SYMMEMTRIC, BIDS (only), ASKS (only) 34 | type = SYMMETRIC 35 | 36 | [uptrend] 37 | #these are in percentages, 0.1 = 10% - 0.01 = 1% 38 | profitmargin = 0.1 39 | stoploss = 0.01 40 | 41 | [logging] 42 | logfile = mortybot.log 43 | --------------------------------------------------------------------------------