├── utils ├── __init__.py ├── utility.py ├── margin_helper.py └── decorators.py ├── clients ├── __init__.py ├── base.py └── delta.py ├── market_maker ├── __init__.py ├── unhedged.py └── base.py ├── .gitignore ├── requirements.txt ├── config ├── accounts.json ├── dotenvs.py ├── accounts.py └── .env.sample ├── strategy_runner.py ├── ecosystem.config.js ├── README.md └── custom_exceptions.py /utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /clients/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /market_maker/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | .vscode 4 | venv 5 | *.log 6 | .env*% -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | dateparser 2 | requests 3 | websocket-client==0.54.0 4 | python-dotenv 5 | numpy 6 | raven==6.9.0 7 | delta_rest_client 8 | -------------------------------------------------------------------------------- /config/accounts.json: -------------------------------------------------------------------------------- 1 | { 2 | "delta": { 3 | "delta@account.com": { 4 | "api_key": "------api key here-------", 5 | "api_secret": "-----api secret here-----", 6 | "chain": "testnet" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /config/dotenvs.py: -------------------------------------------------------------------------------- 1 | from dotenv import load_dotenv 2 | import os 3 | 4 | 5 | dotenvs = os.getenv('DOTENVS').split(",") 6 | for dotenv in dotenvs: 7 | env_path = os.path.join(os.path.dirname(__file__), 8 | '.env.%s' % dotenv) 9 | load_dotenv(dotenv_path=env_path) 10 | -------------------------------------------------------------------------------- /config/accounts.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | global accounts 5 | accounts = json.loads(open(os.path.join(os.path.dirname(__file__), 6 | 'accounts.json')).read()) 7 | 8 | 9 | def exchange_accounts(exchange): 10 | account_list = os.getenv("%s_ACCOUNTS" % exchange.upper()).split(',') 11 | return list(map(lambda account: accounts[exchange][account], account_list)) 12 | -------------------------------------------------------------------------------- /config/.env.sample: -------------------------------------------------------------------------------- 1 | ############# Required ENV ############## 2 | ######################################### 3 | DELTA_PRODUCT_ID= 4 | MAX_LEVERAGE= 5 | NUM_LEVELS= 6 | MIN_NUM_LEVELS= 7 | 8 | ############# Optional ENV ############## 9 | MARGINING_FACTOR=0.95 10 | DIFF_SIZE_PERCENT=5 11 | DIFF_PRICE_PERCENT=0.05 12 | 13 | BUY_PRICE_SCALING_FACTOR=1.0000 14 | SELL_PRICE_SCALING_FACTOR=1.0000 15 | 16 | SEND_ALERTS=False 17 | LOG_FILE=log/market_maker_bot.log 18 | 19 | 20 | -------------------------------------------------------------------------------- /strategy_runner.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import traceback 4 | from raven import Client 5 | from market_maker.unhedged import UnhedgedMarketMaker 6 | 7 | # from config import dotenv 8 | 9 | 10 | def main(): 11 | strategy = os.getenv('STRATEGY') 12 | if strategy is not None: 13 | strategy_name = strategy.lower() 14 | else: 15 | raise BaseException('Please pass STRATEGY environment variable') 16 | 17 | if strategy_name == 'unhedged': 18 | UnhedgedMarketMaker().run_loop() 19 | 20 | 21 | if __name__ == '__main__': 22 | try: 23 | main() 24 | except Exception as e: 25 | traceback.print_exc() 26 | -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "apps": [{ 3 | "name": "MyBot", 4 | "script": "strategy_runner.py", 5 | "env": { 6 | "DOTENVS": "sample", 7 | "BOT": "MyBot", 8 | "DELTA_ACCOUNTS": "delta@account.com", 9 | "STRATEGY": "unhedged", 10 | "DELTA_PRODUCT_ID": 24, 11 | "DELTA_PRODUCT_SYMBOL": "BTCUSD_28Jun", 12 | "MAX_LEVERAGE": 1, 13 | "IMPACT_SIZE": 1000, 14 | "LOOP_INTERVAL": 3, 15 | "NUM_LEVELS": 5, 16 | "NUM_LEVELS": 6, 17 | "DIFF_SIZE_PERCENT": 0, 18 | "MIN_NUM_LEVELS": 5, 19 | "DIFF_PRICE_PERCENT": 0, 20 | "BUY_PRICE_SCALING_FACTOR": 1.0000, 21 | "SELL_PRICE_SCALING_FACTOR": 1.0000, 22 | "LOG_FILE": "log/MyBot-unhedged.log", 23 | "MIN_LEVEL_SIZE": 100, 24 | "MAX_LEVEL_SIZE": 1000, 25 | "AUTO_TOPUP_THRESHOLD": 10 26 | } 27 | }] 28 | }; -------------------------------------------------------------------------------- /clients/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from decimal import Decimal 3 | 4 | 5 | class BaseClient(ABC): 6 | def __init__(self): 7 | pass 8 | 9 | @abstractmethod 10 | def connect(self): 11 | pass 12 | 13 | @abstractmethod 14 | def connect_rc(self): 15 | pass 16 | 17 | @abstractmethod 18 | def disconnect(self): 19 | pass 20 | 21 | @abstractmethod 22 | def reconnect(self): 23 | pass 24 | 25 | @abstractmethod 26 | def isConnected(self): 27 | pass 28 | 29 | @abstractmethod 30 | def subscribeChannel(self, channel_name, symbol, callback): 31 | pass 32 | 33 | @abstractmethod 34 | def unsubscribeChannel(self, channel_name, callback): 35 | pass 36 | 37 | @abstractmethod 38 | def market_depth(self, symbol): 39 | pass 40 | 41 | @abstractmethod 42 | def funds(self, symbol): 43 | pass 44 | 45 | @abstractmethod 46 | def position(self, symbol): 47 | pass 48 | -------------------------------------------------------------------------------- /utils/utility.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | 3 | 4 | def round_price_by_tick_size(number, tick_size, floor_or_ceil=None): 5 | tick_size = Decimal(tick_size) 6 | number = Decimal(number) 7 | remainder = number % tick_size 8 | if remainder == 0: 9 | number = number 10 | if floor_or_ceil is None: 11 | floor_or_ceil = 'ceil' if (remainder >= tick_size / 2) else 'floor' 12 | if floor_or_ceil == 'ceil': 13 | number = number - remainder + tick_size 14 | else: 15 | number = number - remainder 16 | number_of_decimals = len( 17 | format(Decimal(repr(float(tick_size))), 'f').split('.')[1]) 18 | number = round(number, number_of_decimals) 19 | return number 20 | 21 | 22 | def get_position(size=0, entry_price=None, liquidation_price=None): 23 | return { 24 | 'size': size, 25 | 'entry_price': entry_price, 26 | 'liquidation_price': liquidation_price 27 | } 28 | 29 | 30 | def takePrice(elem): 31 | return Decimal(elem['limit_price']) 32 | 33 | 34 | def takeFirst(elem): 35 | return elem[0] 36 | -------------------------------------------------------------------------------- /utils/margin_helper.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | import operator 3 | import numpy 4 | 5 | 6 | def calculateMarginFromLiquidationPrice(position_size, entry_price, liquidation_price, maintenance_margin, isInverse=False): 7 | if isInverse: 8 | bankruptcy_price = liquidation_price * entry_price / \ 9 | (entry_price + liquidation_price * numpy.sign(position_size) 10 | * maintenance_margin / Decimal(100)) 11 | return -1 * position_size * (1/entry_price - 1/bankruptcy_price) 12 | else: 13 | bankruptcy_price = liquidation_price - \ 14 | numpy.sign(position_size) * entry_price * \ 15 | maintenance_margin / Decimal(100) 16 | return -1 * position_size * (bankruptcy_price - entry_price) 17 | 18 | 19 | def funding_dampener(premium): 20 | return max(Decimal('0.05'), premium) + min(Decimal('-0.05'), premium) 21 | 22 | 23 | def calculatePremiumFromFunding(funding): 24 | if funding > 0: 25 | return funding + Decimal('0.05') 26 | elif funding < 0: 27 | return funding - Decimal('0.05') 28 | else: 29 | return Decimal(0) 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Delta Trading Bots 2 | 3 | Collection of publicly available strategies and trading bots for trading bitcoin futures on delta. 4 | 5 | ## Compatibility 6 | 7 | This module supports Python 3.5 and later. 8 | 9 | ## Disclaimer 10 | 11 | Delta is not responsible for any losses incurred when using this code. This code is intended for sample purposes ONLY - do not use this code for real trades unless you fully understand what it does and what its caveats are. 12 | 13 | Develop on Testnet first! 14 | 15 | ## Project Setup 16 | 17 | 1. Create a [Testnet Delta account](https://testnet.delta.exchange) and deposit some BTC 18 | 19 | 2. Create a new virtualenv and install dependencies 20 | 21 | ``` 22 | virtualenv -p python3 venv 23 | source venv/bin/activate 24 | pip install -r requirements.txt 25 | ``` 26 | 27 | 3. create a log folder 28 | 29 | ``` 30 | mkdir log 31 | ``` 32 | 33 | ## Market Making Bot 34 | 35 | Market Makers profit by charging higher offer prices than bid prices. The difference is called the ‘spread’. The spread compensates the market makers for the risk inherited in such trades. The risk is the price movement against the market makers trading position. 36 | 37 | ### How to run 38 | 39 | 1. You can create multiple configurations for multiple running instances. Lets create a configuration for running the bot on testnet 40 | 41 | 2. Copy `config/.env.sample` to `config/.env.testnet`. You can tweak settings here. 42 | 43 | 3. Edit `config/accounts.json` to enter your account credentials. 44 | 45 | 4. From the root folder, Pass the environment and run `strategy_runner.py` 46 | 47 | 5. You can also run your strategy from pm2, add your bot configurations in `ecosystem.config.js` 48 | ``` 49 | pm2 start ecosystem.config.js --only=MyBot 50 | ``` 51 | 52 | ### How to customize 53 | 54 | 1. Writing your own custom strategy is super easy. Just refer to `market_maker/custom_strategy.py` 55 | 3. Once you define your custom_strategy, add it in strategy runner. 56 | 57 | ``` 58 | from market_maker.custom_strategy import CustomStrategy 59 | mm = CustomStrategy() 60 | mm.run_loop() 61 | ``` 62 | -------------------------------------------------------------------------------- /utils/decorators.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from time import sleep 3 | import socket 4 | import requests 5 | import json 6 | import custom_exceptions 7 | 8 | 9 | def check_requests_error(client, status_code, error_msg=None): 10 | if status_code == 401: 11 | raise custom_exceptions.AuthenticationError(client) 12 | elif status_code == 400: 13 | if 'InsufficientMargin' == error_msg: 14 | raise custom_exceptions.InsufficientMarginError(client) 15 | elif 'InvalidOrder' == error_msg: 16 | raise custom_exceptions.InvalidOrder(client) 17 | elif 'MarketDisrupted' == error_msg: 18 | raise custom_exceptions.MarketDisrupted(client) 19 | else: 20 | raise custom_exceptions.BadRequestError(client) 21 | elif status_code == 422: 22 | raise custom_exceptions.BadRequestError(client) 23 | elif status_code == 429: 24 | raise custom_exceptions.TooManyRequestsError(client) 25 | elif status_code == 501: 26 | raise custom_exceptions.ServiceUnavailabeError(client) 27 | elif status_code == 502: 28 | raise custom_exceptions.ServiceUnavailabeError(client) 29 | elif status_code == 503: 30 | raise custom_exceptions.ServiceUnavailabeError(client) 31 | elif status_code == 504: 32 | raise custom_exceptions.ServiceUnavailabeError(client) 33 | elif status_code == 500: 34 | raise custom_exceptions.ServiceUnavailabeError(client) 35 | else: 36 | raise custom_exceptions.UnknownError(client) 37 | 38 | 39 | def handle_requests_exceptions(client): 40 | def inner_function(make_request): 41 | def wrapper(*args, **kwargs): 42 | logger = args[0].logger 43 | try: 44 | response = make_request(*args, **kwargs) 45 | return response 46 | except requests.exceptions.HTTPError as e: 47 | status_code = e.response.status_code 48 | try: 49 | logger.info('%s Exception: %s ' % 50 | (client, e.response.text)) 51 | error_msg = e.response.json()['error'] 52 | check_requests_error( 53 | client, status_code, error_msg) 54 | except Exception: 55 | logger.info(str(e)) 56 | check_requests_error(client, status_code) 57 | return wraps(make_request)(wrapper) 58 | return inner_function 59 | -------------------------------------------------------------------------------- /custom_exceptions.py: -------------------------------------------------------------------------------- 1 | class Error(Exception): 2 | pass 3 | 4 | 5 | class SocketConnectionError(Error): 6 | def __init__(self, client): 7 | self.client = client 8 | self.status_code = 500 9 | 10 | 11 | class BadGatewayError(Error): 12 | def __init__(self, client): 13 | self.client = client 14 | self.status_code = 502 15 | 16 | 17 | class ServiceUnavailabeError(Error): 18 | def __init__(self, client): 19 | self.client = client 20 | self.status_code = 503 21 | 22 | 23 | class PlaceOrderError(Error): 24 | def __init__(self, client): 25 | self.client = client 26 | self.status_code = 400 27 | 28 | 29 | class InsufficientMarginError(Error): 30 | def __init__(self, client): 31 | self.client = client 32 | self.status_code = 400 33 | 34 | 35 | class LowerThanBankruptcyError(Error): 36 | def __init__(self, client): 37 | self.client = client 38 | self.status_code = 400 39 | 40 | 41 | class LowOrderSizeError(Error): 42 | def __init__(self, client): 43 | self.client = client 44 | self.status_code = 400 45 | 46 | 47 | class ContractExpiredError(Error): 48 | def __init__(self, client): 49 | self.client = client 50 | self.status_code = 400 51 | 52 | 53 | class EditOrderError(Error): 54 | def __init__(self, client): 55 | self.client = client 56 | self.status_code = 400 57 | 58 | 59 | class MarketDisrupted(Error): 60 | def __init__(self, client): 61 | self.client = client 62 | self.status_code = 400 63 | 64 | 65 | class InvalidOrder(Error): 66 | def __init__(self, client): 67 | self.client = client 68 | self.status_code = 400 69 | 70 | 71 | class BadRequestError(Error): 72 | def __init__(self, client): 73 | self.client = client 74 | self.status_code = 400 75 | 76 | 77 | class TooManyRequestsError(Error): 78 | def __init__(self, client): 79 | self.client = client 80 | self.status_code = 429 81 | 82 | 83 | class InternalServerError(Error): 84 | def __init__(self, client): 85 | self.client = client 86 | self.status_code = 503 87 | 88 | 89 | class AuthenticationError(Error): 90 | def __init__(self, client): 91 | self.client = client 92 | self.status_code = 401 93 | 94 | 95 | class ForbiddenError(Error): 96 | def __init__(self, client): 97 | self.client = client 98 | self.status_code = 403 99 | 100 | 101 | class NonceError(Error): 102 | def __init__(self, client): 103 | self.client = client 104 | self.status_code = 401 105 | 106 | 107 | class UnknownError(Error): 108 | def __init__(self, client): 109 | self.client = client 110 | self.status_code = 1000 111 | -------------------------------------------------------------------------------- /market_maker/unhedged.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | from time import sleep 4 | from decimal import Decimal 5 | from market_maker.base import BaseMarketMaker 6 | from clients.delta import Delta, OrderState, OrderType 7 | from delta_rest_client import cancel_order_format 8 | from utils.utility import round_price_by_tick_size 9 | from utils.margin_helper import calculateMarginFromLiquidationPrice, funding_dampener 10 | import operator 11 | 12 | import json 13 | 14 | 15 | class UnhedgedMarketMaker(BaseMarketMaker): 16 | def __init__(self): 17 | self.max_leverage = int(os.getenv('MAX_LEVERAGE')) 18 | self.auto_topup_threshold = Decimal(os.getenv('AUTO_TOPUP_THRESHOLD')) 19 | self.min_level_size = int(os.getenv('MIN_LEVEL_SIZE')) 20 | self.max_level_size = int(os.getenv('MAX_LEVEL_SIZE')) 21 | super().__init__() 22 | 23 | def delta_setup(self): 24 | channels = ['trading_notifications', 'l2_orderbook', 'positions'] 25 | super().delta_setup(channels=channels) 26 | self.delta.subscribeChannel( 27 | 'mark_price', "MARK:%s" % (self.product['symbol'])) 28 | self.delta.subscribeChannel( 29 | 'spot_price', self.product['spot_index']['symbol']) 30 | 31 | def exit(self, signum, frame): 32 | self.delta.disconnect() 33 | super().exit(signum, frame) 34 | 35 | # Get impact squareoff 36 | def get_impact_squareoff(self, size, orders): 37 | fills = [] 38 | for order in orders: 39 | fill_size = min(size, order['size']) 40 | size = size - fill_size 41 | fills.append([order['price'], fill_size]) 42 | if size == 0: 43 | break 44 | avg_price = sum(map( 45 | lambda x: x[0] * x[1], fills 46 | )) / sum(map(lambda x: x[1], fills)) 47 | return avg_price 48 | 49 | def apply_risk_limits(self, buy_orders, sell_orders): 50 | return buy_orders, sell_orders 51 | 52 | def check_for_position_auto_topup(self): 53 | current_delta_position = self.delta_position() 54 | if current_delta_position['size'] != 0: 55 | current_liquidation_price = current_delta_position['liquidation_price'] 56 | current_mark_price = self.delta.mark_price(self.product_id) 57 | distance = abs(current_liquidation_price - 58 | current_mark_price) * 100 / current_mark_price 59 | if distance < self.auto_topup_threshold: 60 | if current_delta_position['size'] > 0: 61 | new_liquidation_price = current_mark_price * \ 62 | (1 - self.auto_topup_threshold/100) 63 | else: 64 | new_liquidation_price = current_mark_price * \ 65 | (1 + self.auto_topup_threshold/100) 66 | margin = calculateMarginFromLiquidationPrice( 67 | current_delta_position['size'] * 68 | Decimal(self.product['contract_value']), 69 | current_delta_position['entry_price'], 70 | new_liquidation_price, 71 | Decimal(self.product['maintenance_margin']), 72 | isInverse=self.is_inverse_future() 73 | ) 74 | delta_margin = margin - current_delta_position['margin'] 75 | if delta_margin > 0: 76 | funds = self.delta.available_funds() 77 | if funds > delta_margin: 78 | self.logger.info( 79 | 'Liquidation price too close, Auto Top up trigger') 80 | self.delta.addPositionMargin( 81 | self.product_id, str(delta_margin)) 82 | self.logger.info('Auto Top up complete') 83 | else: 84 | message = "Liquidation price too close, but not enough balance for auto top up" 85 | self.logger.info(message) 86 | if os.getenv('SEND_ALERTS').lower() == 'true': 87 | # TODO: Send alert 88 | pass 89 | 90 | def sanity_check(self): 91 | super().sanity_check() 92 | self.logger.info('Sanity check') 93 | if not self.delta.isConnected(): 94 | self.delta.reconnect() 95 | self.check_for_position_auto_topup() 96 | 97 | def pause_trading(self, halt_message=None, halting_time=10, send_email=True): 98 | self.trading_paused = True 99 | if halt_message: 100 | self.logger.info("Trading paused: " + halt_message) 101 | self.cancel_open_orders() 102 | if send_email and os.getenv('SEND_ALERTS').lower() == 'true': 103 | # TODO: send alert 104 | pass 105 | self.logger.info('Sleeping for %d seconds' % halting_time) 106 | sleep(halting_time) 107 | self.trading_paused = False 108 | 109 | def generate_orders(self): 110 | delta_spot_price = self.delta.spot_price( 111 | self.product['spot_index']['symbol'], self.product_id) 112 | buy_orders = [] 113 | sell_orders = [] 114 | for i in range(1, self.num_levels + 1): 115 | buy_orders.append({ 116 | 'price': delta_spot_price - i * self.tick_size, 117 | 'size': random.randint(self.min_level_size, self.max_level_size) 118 | }) 119 | sell_orders.append({ 120 | 'price': delta_spot_price + i * self.tick_size, 121 | 'size': random.randint(self.min_level_size, self.max_level_size) 122 | }) 123 | return True, buy_orders, sell_orders 124 | -------------------------------------------------------------------------------- /clients/delta.py: -------------------------------------------------------------------------------- 1 | from delta_rest_client import DeltaRestClient, create_order_format, round_by_tick_size, cancel_order_format, OrderType 2 | import logging 3 | from decimal import Decimal 4 | import datetime 5 | import time 6 | import threading 7 | import json 8 | import base64 9 | import hmac 10 | import hashlib 11 | from itertools import islice 12 | from time import sleep 13 | import requests 14 | import websocket 15 | from threading import Timer 16 | from enum import Enum 17 | 18 | from utils.utility import get_position, round_price_by_tick_size 19 | from clients.base import BaseClient 20 | import custom_exceptions 21 | from utils.decorators import handle_requests_exceptions 22 | 23 | 24 | def get_time_stamp(): 25 | d = datetime.datetime.utcnow() 26 | epoch = datetime.datetime(1970, 1, 1) 27 | return str(int((d - epoch).total_seconds())) 28 | 29 | 30 | def generate_signature(secret, message): 31 | message = bytes(message, 'utf-8') 32 | secret = bytes(secret, 'utf-8') 33 | hash = hmac.new(secret, message, hashlib.sha256) 34 | return hash.hexdigest() 35 | 36 | # TODO: 37 | # Exception handling in Delta 38 | 39 | 40 | class OrderState(Enum): 41 | OPEN = 'open' 42 | CLOSE = 'closed' 43 | CANCELLED = 'cancelled' 44 | PENDING = 'pending' 45 | 46 | 47 | class Delta(BaseClient): 48 | def __init__(self, account, channels, product_id, callback=None): 49 | super().__init__() 50 | self.logger = logging.getLogger(__name__) 51 | self.timer = Timer(40.0, self.ping) 52 | self.api_key = account['api_key'] 53 | self.api_secret = account['api_secret'] 54 | if account['chain'] == 'testnet': 55 | self.delta_base_url = "https://testnet-api.delta.exchange" 56 | self.ws_endpoint = "wss://testnet-api.delta.exchange:2096" 57 | elif account['chain'] == 'mainnet': 58 | self.delta_base_url = "https://api.delta.exchange" 59 | self.ws_endpoint = "wss://api.delta.exchange:2096" 60 | elif account['chain'] == 'devnet': 61 | self.delta_base_url = "https://devnet-api.delta.exchange" 62 | self.ws_endpoint = "wss://devnet-api.delta.exchange:2096" 63 | else: 64 | raise Exception('InvalidDeltaChain') 65 | 66 | self.channels = channels 67 | self.product_id = product_id 68 | self.callback = callback 69 | self.callbacks = {} 70 | self.data = { 71 | 'positions': {}, 72 | 'mark_price': {}, 73 | 'l2_orderbook': [], 74 | 'spot_price': {} 75 | } 76 | self.last_seen = { 77 | 'positions': {}, 78 | 'mark_price': {}, 79 | 'spot_price': {} 80 | } 81 | self.exited = True 82 | 83 | self.delta_client = self.connect_rc() 84 | 85 | self.product = self.get_product(product_id) 86 | self.contract_size = round_price_by_tick_size( 87 | self.product['contract_value'], self.product['contract_value']) 88 | self.tick_size = round_price_by_tick_size( 89 | self.product['tick_size'], self.product['tick_size']) 90 | self.isInverse = self.product['product_type'] == 'inverse_future' 91 | self.isQuanto = self.product['is_quanto'] 92 | self.symbol = self.product['symbol'] 93 | if self.channels: 94 | self.connect() 95 | 96 | def __auth(self): 97 | if not self.ws: 98 | raise Exception('Need to establish a socket connection first') 99 | method = 'GET' 100 | timestamp = get_time_stamp() 101 | path = '/live' 102 | signature_data = method + timestamp + path 103 | signature = generate_signature(self.api_secret, signature_data) 104 | self.ws.send(json.dumps({ 105 | "type": "auth", 106 | "payload": { 107 | "api-key": self.api_key, 108 | "signature": signature, 109 | "timestamp": timestamp 110 | } 111 | })) 112 | 113 | def __on_error(self, error): 114 | self.logger.info(error) 115 | 116 | def __on_open(self): 117 | self.logger.info("Delta Websocket Opened.") 118 | 119 | def __on_close(self): 120 | self.logger.info('Delta Websocket Closed.') 121 | if not self.exited: 122 | self.reconnect() 123 | 124 | def __on_message(self, message): 125 | self.__restart_ping_timer() 126 | try: 127 | message = json.loads(message) 128 | if 'type' in message: 129 | event = message['type'] 130 | if event == 'l2_orderbook': 131 | self.orderbook_updates(message) 132 | elif event in ['fill', 'self_trade', 'pnl', 'liquidation', 'adl', 'stop_trigger', 'stop_cancel']: 133 | if 'trading_notifications' in self.callbacks: 134 | self.callbacks['trading_notifications'](message) 135 | elif event == 'positions': 136 | self.position_updates(message) 137 | elif event == 'mark_price': 138 | self.mark_price_update(message) 139 | elif event == 'spot_price': 140 | self.spot_price_update(message) 141 | else: 142 | pass 143 | elif 'message' in message: 144 | self.logger.info(message['message']) 145 | except Exception: 146 | pass 147 | 148 | def __restart_ping_timer(self): 149 | if self.timer.isAlive(): 150 | self.timer.cancel() 151 | self.timer = Timer(40.0, self.ping) 152 | self.timer.start() 153 | 154 | def connect_rc(self): 155 | delta_client = DeltaRestClient( 156 | base_url=self.delta_base_url, 157 | api_key=self.api_key, 158 | api_secret=self.api_secret 159 | ) 160 | return delta_client 161 | 162 | def connect(self): 163 | self.ws = websocket.WebSocketApp(self.ws_endpoint, 164 | on_message=self.__on_message, 165 | on_close=self.__on_close, 166 | on_open=self.__on_open, 167 | on_error=self.__on_error) 168 | 169 | self.wst = threading.Thread(name='Delta Websocket', 170 | target=lambda: self.ws.run_forever(ping_interval=30, ping_timeout=10)) 171 | self.wst.daemon = True 172 | self.wst.start() 173 | 174 | conn_timeout = 5 175 | while not self.ws.sock or not self.ws.sock.connected and conn_timeout: 176 | sleep(1) 177 | conn_timeout -= 1 178 | if not conn_timeout: 179 | self.logger.debug( 180 | "Couldn't establish connetion with Delta websocket") 181 | else: 182 | self.exited = False 183 | if self.api_key and self.api_secret: 184 | self.__auth() 185 | sleep(2) 186 | for channel_name in self.channels: 187 | self.subscribeChannel(channel_name, self.symbol, self.callback) 188 | 189 | def is_thread_alive(self): 190 | return (self.wst and self.wst.is_alive()) 191 | 192 | def disconnect(self): 193 | self.exited = True 194 | self.timer.cancel() 195 | self.ws.close() 196 | self.callbacks.clear() 197 | # self.wst.join() 198 | self.logger.info("Delta Websocket Disconnected.") 199 | 200 | def reconnect(self): 201 | self.disconnect() 202 | self.connect() 203 | 204 | def isConnected(self): 205 | return self.ws.sock and self.ws.sock.connected 206 | 207 | def ping(self): 208 | self.ws.send(json.dumps({ 209 | "type": "ping" 210 | })) 211 | 212 | def orderbook_updates(self, result): 213 | orderbooks = list(filter( 214 | lambda x: x['symbol'] == result['symbol'], self.data['l2_orderbook'] 215 | )) 216 | 217 | if orderbooks: 218 | orderbook = orderbooks[0] 219 | orderbook['last_seen'] = time.time() 220 | orderbook['bids'] = result['buy'] 221 | orderbook['asks'] = result['sell'] 222 | else: 223 | orderbook = { 224 | 'last_seen': time.time(), 225 | 'symbol': result['symbol'], 226 | 'asks': result['buy'], 227 | 'bids': result['sell'] 228 | } 229 | self.data['l2_orderbook'].append(orderbook) 230 | 231 | def position_updates(self, result): 232 | product_id = result['product_id'] 233 | position = get_position( 234 | entry_price=round_price_by_tick_size( 235 | result['entry_price'], self.tick_size), 236 | size=int(result['size']) 237 | ) 238 | position['margin'] = Decimal(result['margin']) 239 | position['liquidation_price'] = Decimal(result['liquidation_price']) 240 | self.data['positions'][product_id] = position 241 | self.last_seen['positions'][product_id] = time.time() 242 | 243 | def mark_price_update(self, result): 244 | product_id = result['product_id'] 245 | self.data['mark_price'][product_id] = Decimal(result['price']) 246 | self.last_seen['mark_price'][product_id] = time.time() 247 | 248 | def spot_price_update(self, result): 249 | spot_symbol = result['symbol'] 250 | self.data['spot_price'][spot_symbol] = Decimal(result['price']) 251 | self.last_seen['spot_price'][spot_symbol] = time.time() 252 | 253 | """ ******* SOCKET METHODS ******* """ 254 | 255 | def subscribeChannel(self, channel_name, symbol, callback=None): 256 | self.ws.send(json.dumps({ 257 | "type": "subscribe", 258 | "payload": { 259 | "channels": [ 260 | { 261 | "name": channel_name, 262 | "symbols": [symbol] 263 | } 264 | ] 265 | } 266 | })) 267 | self.callbacks[channel_name] = callback 268 | 269 | def unsubscribeChannel(self, channel_name): 270 | self.ws.send(json.dumps({ 271 | "type": "unsubscribe", 272 | "channels": [ 273 | { 274 | "name": channel_name, 275 | "symbols": [self.symbol] 276 | } 277 | ] 278 | } 279 | )) 280 | if channel_name in self.callbacks: 281 | del self.callbacks[channel_name] 282 | 283 | def market_depth(self, symbol): 284 | if self.isConnected() and 'l2_orderbook' in self.data: 285 | orderbooks = list(filter( 286 | lambda x: x['symbol'] == symbol, self.data['l2_orderbook'] 287 | )) 288 | if orderbooks and time.time() - orderbooks[0]['last_seen'] < 2: 289 | asks = list(map( 290 | lambda x: { 291 | 'price': round_price_by_tick_size(x['limit_price'], self.tick_size), 292 | 'size': int(x['size']) 293 | }, orderbooks[0]['asks'] 294 | )) 295 | bids = list(map( 296 | lambda x: { 297 | 'price': round_price_by_tick_size(x['limit_price'], self.tick_size), 298 | 'size': int(x['size']) 299 | }, orderbooks[0]['bids'] 300 | )) 301 | bids = list(filter( 302 | lambda x: Decimal(x['price']) > self.tick_size, 303 | bids 304 | )) 305 | asks.sort(key=lambda x: x['price']) 306 | bids.sort(key=lambda x: x['price'], reverse=True) 307 | return {'asks': asks, 'bids': bids} 308 | return self.getorderbook(self.product_id) 309 | 310 | def mark_price(self, product_id): 311 | if self.isConnected() and 'mark_price' in self.data: 312 | if product_id in self.data['mark_price'] and time.time() - self.last_seen['mark_price'][product_id] < 15: 313 | return self.data['mark_price'][product_id] 314 | return self.get_mark_price(product_id) 315 | 316 | def spot_price(self, spot_symbol, product_id): 317 | if self.isConnected() and 'spot_price' in self.data: 318 | if spot_symbol in self.data['spot_price'] and time.time() - self.last_seen['spot_price'][spot_symbol] < 15: 319 | return self.data['spot_price'][spot_symbol] 320 | return self.get_spot_price(product_id) 321 | 322 | def position(self, product_id): 323 | # if self.isConnected(): 324 | # if product_id in self.data['positions'] and time.time() - self.last_seen['positions'][product_id] < 15: 325 | # return self.data['positions'][product_id] 326 | # Get position from rest and set locally 327 | position = self.get_position_over_rest(product_id) 328 | self.data['positions'][product_id] = position 329 | self.last_seen['positions'][product_id] = time.time() 330 | return position 331 | 332 | """ ******* HTTP METHODS ******* """ 333 | 334 | @handle_requests_exceptions(client='Delta') 335 | def getorderbook(self, product_id): 336 | orderbook = self.delta_client.get_L2_orders(product_id, auth=True) 337 | asks = list(map( 338 | lambda x: { 339 | 'price': round_price_by_tick_size(x['price'], self.tick_size), 340 | 'size': int(x['size']) 341 | }, orderbook['sell_book'] 342 | )) 343 | bids = list(map( 344 | lambda x: { 345 | 'price': round_price_by_tick_size(x['price'], self.tick_size), 346 | 'size': int(x['size']) 347 | }, orderbook['buy_book'] 348 | )) 349 | asks.sort(key=lambda x: x['price']) 350 | bids.sort(key=lambda x: x['price'], reverse=True) 351 | return {'asks': asks, 'bids': bids} 352 | 353 | @handle_requests_exceptions(client='Delta') 354 | def get_mark_price(self, product_id): 355 | mark_price = self.delta_client.get_mark_price(product_id, auth=True) 356 | return Decimal(mark_price) 357 | 358 | @handle_requests_exceptions(client='Delta') 359 | def get_spot_price(self, product_id): 360 | orderbook = self.delta_client.get_L2_orders(product_id, auth=True) 361 | spot_price = orderbook['spot_price'] 362 | return Decimal(spot_price) 363 | 364 | @handle_requests_exceptions(client='Delta') 365 | def get_position_over_rest(self, product_id): 366 | positions = self.delta_client.get_position(product_id) 367 | if positions and int(positions['size']) != 0: 368 | position = get_position( 369 | entry_price=round_price_by_tick_size( 370 | positions['entry_price'], self.tick_size), 371 | size=int(positions['size']) 372 | ) 373 | position['margin'] = Decimal(positions['margin']) 374 | position['liquidation_price'] = Decimal( 375 | positions['liquidation_price']) 376 | return position 377 | else: 378 | # RETURN EMPTY POSITION ONLY IF NO OPEN POSITIONS IS FOUND 379 | position = get_position() 380 | position['margin'] = 0 381 | position['liquidation_price'] = None 382 | self.logger.info(position) 383 | return position 384 | 385 | @handle_requests_exceptions(client='Delta') 386 | def addPositionMargin(self, product_id, delta_margin): 387 | return self.delta_client.change_position_margin(product_id, delta_margin) 388 | 389 | @handle_requests_exceptions(client='Delta') 390 | def funds(self): 391 | response = self.delta_client.get_wallet( 392 | self.product['settling_asset']['id']) 393 | return Decimal(response['balance']) - Decimal(response['position_margin']) 394 | 395 | @handle_requests_exceptions(client='Delta') 396 | def available_funds(self): 397 | response = self.delta_client.get_wallet( 398 | self.product['settling_asset']['id']) 399 | return Decimal(response['balance']) - Decimal(response['position_margin']) - Decimal(response['order_margin']) - Decimal(response['commission']) 400 | 401 | @handle_requests_exceptions(client='Delta') 402 | def get_product(self, product_id): 403 | response = self.delta_client.request("GET", "products", auth=True) 404 | response = response.json() 405 | products = list( 406 | filter(lambda x: x['id'] == product_id, response)) 407 | return products[0] if len(products) > 0 else None 408 | 409 | @handle_requests_exceptions(client='Delta') 410 | def get_open_orders(self, product_id, state=OrderState.OPEN, page_num=1, page_size=500): 411 | query = { 412 | 'product_id': product_id, 413 | 'state': state.value, 414 | 'page_num': page_num, 415 | 'page_size': page_size 416 | } 417 | if state.value == 'pending': 418 | query['stop_order_type'] = 'stop_loss_order' 419 | response = self.delta_client.get_orders(query) 420 | return response 421 | 422 | @handle_requests_exceptions(client='Delta') 423 | def get_ticker(self, product_id): 424 | response = self.delta_client.get_ticker(product_id) 425 | return response 426 | 427 | @handle_requests_exceptions(client='Delta') 428 | def set_leverage(self, product_id, leverage): 429 | response = self.delta_client.set_leverage( 430 | product_id=product_id, leverage=leverage) 431 | return response 432 | 433 | def market_order(self, size, product_id): 434 | side = 'buy' if size > 0 else 'sell' 435 | if size != 0: 436 | order = { 437 | 'size': abs(size), 438 | 'side': side, 439 | 'order_type': 'market_order', 440 | 'product_id': product_id 441 | } 442 | self.place_order(order) 443 | 444 | @handle_requests_exceptions(client='Delta') 445 | def place_order(self, order): 446 | response = self.delta_client.create_order(order) 447 | if order['order_type'] == 'market_order' and response['unfilled_size'] != 0: 448 | self.logger.info('Delta exception: %s' % str(response)) 449 | raise custom_exceptions.PlaceOrderError('Delta') 450 | else: 451 | return response 452 | 453 | @handle_requests_exceptions(client='Delta') 454 | def create_stop_order(self, product_id, size, stop_price): 455 | side = 'buy' if size > 0 else 'sell' 456 | response = self.delta_client.place_stop_order( 457 | product_id=product_id, size=abs(int(size)), side=side, stop_price=str(stop_price), order_type=OrderType.MARKET) 458 | return response 459 | 460 | @handle_requests_exceptions(client='Delta') 461 | def edit_order(self, product_id, order_id, size, stop_price): 462 | order = { 463 | 'id': order_id, 464 | 'product_id': product_id, 465 | 'size': size, 466 | 'stop_price': str(stop_price) 467 | } 468 | response = self.delta_client.request("PUT", "orders", order) 469 | return response 470 | 471 | @handle_requests_exceptions(client='Delta') 472 | def batch_create(self, product_id, orders): 473 | create_batches = self.slice_orders(orders) 474 | for create_order in create_batches: 475 | self.delta_client.batch_create( 476 | product_id, list(create_order)) 477 | self.logger.info('Created orders: %s, batch size: %s' % 478 | (len(orders), len(create_batches))) 479 | 480 | @handle_requests_exceptions(client='Delta') 481 | def batch_cancel(self, product_id, orders): 482 | delete_batches = self.slice_orders(orders) 483 | for delete_order in delete_batches: 484 | self.delta_client.batch_cancel(self.product_id, 485 | list(map( 486 | cancel_order_format, list( 487 | delete_order) 488 | )) 489 | ) 490 | self.logger.info('Deleted orders: %s, batch size: %s' % 491 | (len(orders), len(delete_batches))) 492 | 493 | @handle_requests_exceptions(client='Delta') 494 | def batch_edit(self, product_id, orders): 495 | edit_batches = self.slice_orders(orders, size=20) 496 | for edit_order in edit_batches: 497 | self.delta_client.batch_edit(product_id, list(edit_order)) 498 | self.logger.info('Edited orders: %s' % (len(orders))) 499 | 500 | def slice_orders(self, orders, size=5): 501 | orders = iter(orders) 502 | return list(iter(lambda: tuple(islice(orders, size)), ())) 503 | -------------------------------------------------------------------------------- /market_maker/base.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from time import sleep 3 | import signal 4 | import sys 5 | import os 6 | import random 7 | import traceback 8 | import requests 9 | from decimal import Decimal 10 | import operator 11 | import socket 12 | from functools import wraps 13 | import math 14 | 15 | import json 16 | from clients.delta import Delta 17 | from delta_rest_client import create_order_format, round_by_tick_size 18 | 19 | from utils.utility import takePrice, round_price_by_tick_size 20 | from abc import ABC, abstractmethod 21 | from config import accounts 22 | import custom_exceptions 23 | 24 | 25 | class BaseMarketMaker(ABC): 26 | def __init__(self): 27 | self.trading_paused = False 28 | self.exit_run_loop = False 29 | self.bot_name = os.getenv("BOT") 30 | logfile = os.getenv('LOG_FILE') 31 | self.setup_logger(logfile) 32 | self.num_levels = int(os.getenv('NUM_LEVELS')) 33 | self.min_levels = int(os.getenv('MIN_NUM_LEVELS')) 34 | self.diff_size_percent = Decimal(os.getenv('DIFF_SIZE_PERCENT')) 35 | self.diff_price_percent = Decimal( 36 | os.getenv('DIFF_PRICE_PERCENT')) 37 | self.post_only = os.getenv('POST_ONLY') 38 | self.loop_interval = float(os.getenv('LOOP_INTERVAL')) 39 | self.buy_price_scale_factor = Decimal( 40 | os.getenv('BUY_PRICE_SCALING_FACTOR')) 41 | self.sell_price_scale_factor = Decimal( 42 | os.getenv('SELL_PRICE_SCALING_FACTOR')) 43 | self.delta_product_symbol = os.getenv("DELTA_PRODUCT_SYMBOL") 44 | signal.signal(signal.SIGINT, self.exit) 45 | self.delta_setup() 46 | 47 | @abstractmethod 48 | def generate_orders(self): 49 | raise NotImplementedError 50 | 51 | @abstractmethod 52 | def sanity_check(self): 53 | pass 54 | 55 | # Check for risk limits and filter orders as per allowed limits 56 | @abstractmethod 57 | def apply_risk_limits(self, buy_orders, sell_orders): 58 | raise NotImplementedError 59 | 60 | def delta_setup(self, channels=[], callback=None): 61 | maker_account = accounts.exchange_accounts('delta')[0] 62 | self.product_id = int(os.getenv('DELTA_PRODUCT_ID')) 63 | self.delta = Delta( 64 | account=maker_account, 65 | channels=channels, 66 | product_id=self.product_id, 67 | callback=callback 68 | ) 69 | self.product = self.delta.get_product(self.product_id) 70 | self.tick_size = round_price_by_tick_size( 71 | self.product['tick_size'], self.product['tick_size']) 72 | self.logger.info(self.tick_size) 73 | self.cancel_open_orders() 74 | 75 | # Set order leverage on delta 76 | max_delta_leverage = os.getenv("MAX_LEVERAGE") 77 | self.delta.set_leverage( 78 | product_id=self.product_id, leverage=max_delta_leverage) 79 | 80 | def is_inverse_future(self): 81 | return self.product['product_type'] == 'inverse_future' 82 | 83 | def is_perpetual(self): 84 | return self.product['contract_type'] == 'perpetual_futures' 85 | 86 | def setup_logger(self, logfile): 87 | # Remove old logger handlers if any 88 | if hasattr(self, 'logger'): 89 | getattr(self, 'logger').handlers = [] 90 | formatter = logging.Formatter(fmt='%(asctime)s - %(levelname)s - %(message)s', 91 | datefmt='%Y-%m-%d %H:%M:%S') 92 | handler = logging.FileHandler(logfile, mode='a') 93 | handler.setFormatter(formatter) 94 | screen_handler = logging.StreamHandler(stream=sys.stdout) 95 | screen_handler.setFormatter(formatter) 96 | self.logger = logging.getLogger() 97 | self.logger.setLevel(logging.INFO) 98 | # if self.logger.handlers: 99 | # print('Logger exists-------------------------------------------------') 100 | self.logger.handlers = [] 101 | self.logger.addHandler(handler) 102 | self.logger.addHandler(screen_handler) 103 | 104 | def notional(self, size, price): 105 | if size == 0 or price is None: 106 | return Decimal(0) 107 | else: 108 | return Decimal(size) * (1 / Decimal(price) 109 | if self.is_inverse_future() else Decimal(price)) 110 | 111 | def get_open_orders(self): 112 | return self.delta.get_open_orders(self.product_id) 113 | 114 | def cancel_open_orders(self): 115 | open_orders = self.get_open_orders() 116 | self.logger.info('Cancelling %d open order on delta' % 117 | len(open_orders)) 118 | 119 | self.delta.batch_cancel(self.product_id, open_orders) 120 | 121 | def delta_position(self): 122 | current_position_delta = self.delta.position(self.product_id) 123 | size = current_position_delta['size'] 124 | if size != 0: 125 | size = current_position_delta['size'] * self.delta.contract_size 126 | current_position_delta['size'] = round_price_by_tick_size( 127 | size, self.delta.contract_size 128 | ) 129 | current_position_delta['notional'] = self.notional( 130 | current_position_delta['size'], current_position_delta['entry_price']) 131 | return current_position_delta 132 | 133 | def delta_margin(self): 134 | return self.delta.funds() 135 | 136 | def diff(self, old_order, new_order): 137 | # TODO : handle sign for buy/sell 138 | # TODO : handle change in quantity 139 | if old_order['size'] == 0: 140 | return 3 141 | change_price = (takePrice(new_order) - 142 | takePrice(old_order)) * Decimal(100.0000) / takePrice(old_order) 143 | change_size = (new_order['size'] - old_order['size'] 144 | ) * 100.0000 / old_order['size'] 145 | 146 | if abs(change_size) > self.diff_size_percent and abs(change_price) <= self.diff_price_percent: 147 | return 2 # Size has changed significantly, price is same 148 | elif abs(change_price) > self.diff_price_percent: 149 | self.logger.debug('Price diff %s %s' % 150 | (change_price, self.diff_price_percent)) 151 | return 1 # Price has changed significantly 152 | else: 153 | return 0 # Levels are same, no need to recreate 154 | 155 | def calculate_orders_diff(self, side, old_orders, new_orders): 156 | orders_to_create = [] 157 | orders_to_delete = [] 158 | 159 | ops = { 160 | "<": operator.lt, 161 | ">": operator.gt 162 | } 163 | 164 | if side == 'buy': 165 | comparision = "<" 166 | else: 167 | comparision = ">" 168 | 169 | i = 0 170 | j = 0 171 | level_index = 0 172 | 173 | while i < len(old_orders) and j < len(new_orders) and level_index < self.num_levels: 174 | diff_level = self.diff( 175 | old_order=old_orders[i], new_order=new_orders[j]) 176 | self.logger.debug('DIFF %s %s %s %d' % 177 | (side, takePrice(old_orders[i]), takePrice(new_orders[j]), diff_level)) 178 | if diff_level == 2: 179 | # Size has changed 180 | orders_to_create.append(new_orders[j]) 181 | orders_to_delete.append(old_orders[i]) 182 | level_index += 1 183 | i += 1 184 | j += 1 185 | elif diff_level == 1: 186 | if ops[comparision](takePrice(old_orders[i]), takePrice(new_orders[j])): 187 | orders_to_create.append(new_orders[j]) 188 | level_index += 1 189 | j += 1 190 | else: 191 | orders_to_delete.append(old_orders[i]) 192 | i += 1 193 | else: 194 | j += 1 195 | i += 1 196 | level_index += 1 197 | while i < len(old_orders): 198 | orders_to_delete.append(old_orders[i]) 199 | i += 1 200 | while j < len(new_orders) and level_index < self.num_levels: 201 | orders_to_create.append(new_orders[j]) 202 | level_index += 1 203 | j += 1 204 | 205 | return (orders_to_create, orders_to_delete) 206 | 207 | def converge_orders(self, buy_orders, sell_orders, max_bid=None, min_ask=None): 208 | # Get all open orders from delta 209 | old_open_orders = self.get_open_orders() 210 | for order in old_open_orders: 211 | order['size'] = order['unfilled_size'] 212 | # filter current asks and bids from open orders 213 | old_sell_orders = list( 214 | filter(lambda x: x['side'] == 'sell', old_open_orders)) 215 | old_sell_orders.sort(key=takePrice) 216 | old_buy_orders = list( 217 | filter(lambda x: x['side'] == 'buy', old_open_orders)) 218 | old_buy_orders.sort(key=takePrice, reverse=True) 219 | 220 | orders_to_create = [] 221 | orders_to_delete = [] 222 | orders_to_edit = [] 223 | 224 | if max_bid: 225 | # Collect all old buy orders with price > max_bid and add them to delete list 226 | while len(old_buy_orders) > 0: 227 | if Decimal(takePrice(old_buy_orders[0])) > max_bid: 228 | orders_to_delete.append(old_buy_orders.pop(0)) 229 | else: 230 | break 231 | # remove all buy orders where price > max_bid 232 | buy_orders = list(filter(lambda x: Decimal( 233 | takePrice(x)) <= max_bid, buy_orders)) 234 | 235 | if min_ask: 236 | # Collect all old sell orders with price < min_ask and add them to delete list 237 | while len(old_sell_orders) > 0: 238 | if Decimal(takePrice(old_sell_orders[0])) < min_ask: 239 | orders_to_delete.append(old_sell_orders.pop(0)) 240 | else: 241 | break 242 | # remove all sell orders where price < min_ask 243 | sell_orders = list(filter(lambda x: Decimal( 244 | takePrice(x)) >= min_ask, sell_orders)) 245 | 246 | buy_create, buy_delete = self.calculate_orders_diff( 247 | side='buy', 248 | old_orders=old_buy_orders, 249 | new_orders=buy_orders 250 | ) 251 | 252 | sell_create, sell_delete = self.calculate_orders_diff( 253 | side='sell', 254 | old_orders=old_sell_orders, 255 | new_orders=sell_orders 256 | ) 257 | if len(sell_create) > 0 and len(sell_delete) > 0: 258 | trend = self.market_trend( 259 | sell_create, sell_delete, side='sell') 260 | elif len(buy_create) > 0 and len(buy_delete) > 0: 261 | trend = self.market_trend( 262 | buy_create, buy_delete, side='buy') 263 | else: 264 | trend = 0 265 | buy_edit, buy_create, buy_delete = self.get_orders( 266 | buy_create, buy_delete) 267 | sell_edit, sell_create, sell_delete = self.get_orders( 268 | sell_create, sell_delete) 269 | 270 | # Downtrend 271 | if trend == -1: 272 | orders_to_edit = orders_to_edit + buy_edit + sell_edit 273 | # Uptrend 274 | elif trend == 1: 275 | orders_to_edit = orders_to_edit + sell_edit + buy_edit 276 | else: 277 | orders_to_edit = orders_to_edit + sell_edit + buy_edit 278 | 279 | orders_to_create = orders_to_create + sell_create + buy_create 280 | orders_to_delete = orders_to_delete + sell_delete + buy_delete 281 | 282 | self.logger.info('Orders to be deleted %d' % len(orders_to_delete)) 283 | self.logger.info('Orders to be edited %d' % len(orders_to_edit)) 284 | self.logger.info('Orders to be created %d' % len(orders_to_create)) 285 | 286 | if orders_to_delete: 287 | self.delta.batch_cancel(self.product_id, orders_to_delete) 288 | if orders_to_edit: 289 | self.delta.batch_edit(self.product_id, orders_to_edit) 290 | if orders_to_create: 291 | self.delta.batch_create(self.product_id, orders_to_create) 292 | 293 | def market_trend(self, create_orders, delete_orders, side='sell'): 294 | if side == 'sell': 295 | op = min 296 | else: 297 | op = max 298 | old_best_price = Decimal(op(map( 299 | lambda x: x['limit_price'], delete_orders 300 | ))) 301 | new_best_price = Decimal(op(map( 302 | lambda x: x['limit_price'], create_orders 303 | ))) 304 | price_diff = new_best_price - old_best_price 305 | if price_diff > 0: 306 | return 1 307 | elif price_diff < 0: 308 | return -1 309 | else: 310 | return 0 311 | 312 | def get_orders(self, create_orders, delete_orders): 313 | orders_to_edit = [] 314 | orders_to_create = [] 315 | orders_to_delete = [] 316 | 317 | def edit_append(create_order, delete_order, edit_orders): 318 | edit_orders.append({ 319 | 'id': delete_order['id'], 320 | 'product_id': create_order['product_id'], 321 | 'limit_price': create_order['limit_price'], 322 | 'unfilled_size': create_order['size'], 323 | 'post_only': self.post_only 324 | }) 325 | delete_orders = list(filter( 326 | lambda o: o.update( 327 | {'notional': str(self.notional(o['size'], o['limit_price']))}) or o, delete_orders 328 | )) 329 | create_orders = list(filter( 330 | lambda o: o.update( 331 | {'notional': str(self.notional(o['size'], o['limit_price']))}) or o, create_orders 332 | )) 333 | extra_notional = 0 334 | while len(create_orders) > 0 and len(delete_orders) > 0: 335 | delete_order = delete_orders.pop(0) 336 | create_order = create_orders.pop(0) 337 | create_order_notional = Decimal(create_order['notional']) 338 | delete_order_notional = Decimal(delete_order['notional']) 339 | if create_order_notional > delete_order_notional + extra_notional: 340 | extra_notional += delete_order_notional 341 | orders_to_delete.append(delete_order) 342 | create_orders.insert(0, create_order) 343 | else: 344 | edit_append(create_order, delete_order, orders_to_edit) 345 | extra_notional += delete_order_notional - \ 346 | create_order_notional 347 | 348 | orders_to_create += create_orders 349 | orders_to_delete += delete_orders 350 | return orders_to_edit, orders_to_create, orders_to_delete 351 | 352 | # Create order format for buy and sell orders 353 | def create_order_format(self, buy_orders, sell_orders): 354 | buy_price_scale_factor = self.buy_price_scale_factor 355 | sell_price_scale_factor = self.sell_price_scale_factor 356 | buy_orders = list(filter( 357 | lambda x: x['size'] >= self.delta.contract_size, buy_orders)) 358 | sell_orders = list(filter( 359 | lambda x: x['size'] >= self.delta.contract_size, sell_orders)) 360 | 361 | for buy_order in buy_orders: 362 | buy_order['size'] /= self.delta.contract_size 363 | buy_order['size'] = int(buy_order['size']) 364 | 365 | for sell_order in sell_orders: 366 | sell_order['size'] /= self.delta.contract_size 367 | sell_order['size'] = int(sell_order['size']) 368 | 369 | buy_orders = list(map( 370 | lambda o: create_order_format( 371 | price=round_by_tick_size( 372 | Decimal(o['price']) * 373 | buy_price_scale_factor, 374 | self.tick_size, 375 | 'floor' 376 | ), 377 | size=o['size'], side='buy', product_id=self.product_id, 378 | post_only=self.post_only 379 | ), buy_orders)) 380 | sell_orders = list(map( 381 | lambda o: create_order_format( 382 | price=round_by_tick_size( 383 | Decimal(o['price']) * 384 | sell_price_scale_factor, 385 | self.tick_size, 386 | 'ceil' 387 | ), 388 | size=o['size'], side='sell', product_id=self.product_id, 389 | post_only=self.post_only 390 | ), sell_orders)) 391 | return buy_orders, sell_orders 392 | 393 | def stop_trading(self, halt_message=None, send_email=True): 394 | self.cancel_open_orders() 395 | if halt_message: 396 | self.logger.info("Trading stopped: " + halt_message) 397 | if send_email and os.getenv('SEND_ALERTS'): 398 | slack(message=halt_message) 399 | sys.exit() 400 | 401 | def pause_trading(self, halt_message=None, halting_time=10, send_email=True): 402 | self.trading_paused = True 403 | # TODO : Avoid race conditions with loop 404 | if halt_message: 405 | self.logger.info("Trading paused: " + halt_message) 406 | self.cancel_open_orders() 407 | if send_email and os.getenv('SEND_ALERTS').lower() == 'true': 408 | # TODO: Send alert 409 | pass 410 | self.logger.info('Sleeping for %d seconds' % halting_time) 411 | sleep(halting_time) 412 | self.trading_paused = False 413 | 414 | def exit(self, signum, frame): 415 | self.stop_trading("Manual Termination", send_email=False) 416 | 417 | def get_top_orders_by_size(self, orders, size, enforce_min_levels=False): 418 | min_size_per_level = Decimal(size / self.min_levels) 419 | selected_orders = [] 420 | leftover_orders = [] 421 | while size > 0 and len(orders) > 0: 422 | order = orders.pop(0) 423 | size_limit = min_size_per_level 424 | level_size = min(size_limit, 425 | size) if enforce_min_levels else size 426 | if order['size'] > level_size: 427 | selected_orders.append({ 428 | 'price': order['price'], 429 | 'size': level_size 430 | }) 431 | order['size'] -= level_size 432 | size = size - level_size 433 | leftover_orders.insert(0, order) 434 | else: 435 | size = size - order['size'] 436 | selected_orders.append(order) 437 | for order in leftover_orders: 438 | orders.insert(0, order) 439 | return selected_orders 440 | 441 | def get_top_orders_by_notional(self, orders, notional, enforce_min_levels=False): 442 | max_notional_per_level = notional / self.min_levels 443 | selected_orders = [] 444 | leftover_orders = [] 445 | while notional > 0 and len(orders) > 0: 446 | order = orders.pop(0) 447 | notional_limit = max_notional_per_level 448 | level_notional = min(notional_limit, 449 | notional) if enforce_min_levels else notional 450 | order_notional = self.notional(order['size'], order['price']) 451 | if order_notional > level_notional: 452 | new_size = order['size'] * level_notional / order_notional 453 | if new_size > 0: 454 | order['size'] -= new_size 455 | selected_orders.append({ 456 | 'price': order['price'], 457 | 'size': new_size 458 | }) 459 | notional = notional - \ 460 | self.notional(new_size, order['price']) 461 | if order['size'] > 0: 462 | leftover_orders.insert(0, order) 463 | else: 464 | notional = notional - order_notional 465 | selected_orders.append(order) 466 | for order in leftover_orders: 467 | orders.insert(0, order) 468 | return selected_orders, notional 469 | 470 | def merge_levels(self, orders): 471 | merged_orders = [] 472 | last_order = None 473 | for order in orders: 474 | if last_order is None: 475 | last_order = order 476 | elif last_order['price'] == order['price']: 477 | last_order['size'] = last_order['size'] + order['size'] 478 | else: 479 | merged_orders.append(last_order) 480 | last_order = order 481 | if last_order is not None: 482 | merged_orders.append(last_order) 483 | return merged_orders 484 | 485 | def run_loop(self): 486 | while True: 487 | self.logger.info( 488 | '----------------Run loop started--------------') 489 | try: 490 | if not self.trading_paused: 491 | should_update, buy_orders, sell_orders = self.generate_orders() 492 | if should_update: 493 | buy_orders, sell_orders = self.apply_risk_limits( 494 | buy_orders, sell_orders) 495 | buy_orders, sell_orders = self.merge_levels( 496 | buy_orders), self.merge_levels(sell_orders) 497 | buy_orders, sell_orders = self.create_order_format( 498 | buy_orders, sell_orders) 499 | self.converge_orders(buy_orders, sell_orders) 500 | sleep(self.loop_interval) 501 | except socket.timeout as e: 502 | self.logger.info(str(e)) 503 | sleep(2) 504 | except custom_exceptions.InsufficientMarginError as e: 505 | message = '%s exception raised at %s. Insufficient Margin Error, Status: %s' % ( 506 | self.bot_name, e.client, e.status_code) 507 | self.pause_trading(halt_message=message, halting_time=5) 508 | except (custom_exceptions.LowerThanBankruptcyError, custom_exceptions.LowOrderSizeError, custom_exceptions.InvalidOrder, custom_exceptions.EditOrderError) as e: 509 | message = '%s exception raised at %s. Bad Request Error, Status: %s' % ( 510 | self.bot_name, e.client, e.status_code) 511 | self.logger.info(message) 512 | sleep(self.loop_interval) 513 | except (custom_exceptions.NonceError, custom_exceptions.AuthenticationError, custom_exceptions.ContractExpiredError) as e: 514 | message = '%s exception raised at %s. Authentication Error, Status: %s' % ( 515 | self.bot_name, e.client, e.status_code) 516 | self.pause_trading(halt_message=message, halting_time=2) 517 | except custom_exceptions.TooManyRequestsError as e: 518 | message = '%s exception raised at %s. TooManyRequestsError, Status: %s' % ( 519 | self.bot_name, e.client, e.status_code) 520 | self.logger.info(message) 521 | sleep(20) 522 | except (custom_exceptions.BadGatewayError, custom_exceptions.ServiceUnavailabeError, custom_exceptions.MarketDisrupted, custom_exceptions.InternalServerError) as e: 523 | message = '%s exception raised at %s. ServerError, Status: %s' % ( 524 | self.bot_name, e.client, e.status_code) 525 | self.logger.info(message) 526 | sleep(5) 527 | except custom_exceptions.BadRequestError as e: 528 | message = '%s exception raised at %s. BadRequestError, Status: %s' % ( 529 | self.bot_name, e.client, e.status_code) 530 | self.pause_trading(halt_message=message, halting_time=5) 531 | except custom_exceptions.UnknownError as e: 532 | message = '%s exception raised at %s. UnknownError, Status: %s' % ( 533 | self.bot_name, e.client, e.status_code) 534 | self.pause_trading(halt_message=message, halting_time=5) 535 | except Exception as e: 536 | message = '%s exception raised.Message: %s' % ( 537 | self.bot_name, str(e)) 538 | traceback.print_exc() 539 | while True: 540 | try: 541 | self.pause_trading( 542 | halt_message=message, halting_time=5) 543 | break 544 | except Exception as e1: 545 | self.logger.info( 546 | 'Exception raised while pausing : %s' % str(e1)) 547 | pass 548 | --------------------------------------------------------------------------------