def fromAdmin(self, message, sessionID): if self.verbose: print(f'[fromAdmin] {sessionID} | {read_FIX_message(message)}') log(self.logger, f'[fromAdmin] {sessionID} | {read_FIX_message(message)}') msgType = fix.MsgType() message.getHeader().getField(msgType) base_str = self._server_str + ' ' + sessionID.toString() + ' | ' if msgType.getValue() == fix.MsgType_Heartbeat: if self.verbose: print(base_str + 'Heartbeat, right back at ya!') elif msgType.getValue() == fix.MsgType_Logon: print(base_str + 'Hello there, good to have you back!') elif msgType.getValue() == fix.MsgType_Logout: print(base_str + 'Logout. See you later!') elif self.verbose: print(f'[fromAdmin] {sessionID} | {read_FIX_message(message)}') if msgType.getValue() != fix.MsgType_SequenceReset: print('unknown message type: ', msgType)
def fromApp(self, message, sessionID): if self.verbose: print(f'[fromApp] {sessionID} | {read_FIX_message(message)}') log(self.logger, f'[fromApp] {sessionID} | {read_FIX_message(message)}') # Get incoming message Type msgType = fix.MsgType() message.getHeader().getField(msgType) msgType = msgType.getValue() # Get timestamp (tag 52) sending_time = extract_message_field_value(fix.SendingTime(), message, 'datetime') # print('sending_time:', sending_time) ########## Quote messages ########## if msgType == fix.MsgType_MassQuote: self.parse_MassQuote(message, sending_time) elif msgType == fix.MsgType_MarketDataSnapshotFullRefresh: self.parse_MarketDataSnapshotFullRefresh(message, sending_time) # 3) Process MarketDataSnapshot_IncrementalRefresh message elif msgType == fix.MsgType_MarketDataIncrementalRefresh: print(self._server_str + ' {MD} INCREMENTAL REFRESH!') ########## Trade messages ########## elif msgType == fix.MsgType_ExecutionReport: self.parse_ExecutionReport(message, sending_time) elif msgType == fix.MsgType_OrderCancelReject: # An OrderCancelReject will be sent as an answer to an OrderCancelRequest, which cannot be executed. # Not much to do here as our order dict would stay the same. # If it was canceled successfully, we should get an execution report. ClOrdID = extract_message_field_value(fix.ClOrdID(), message, 'int') print( f'[fromApp] Order Cancel Request Rejected for order: {ClOrdID}' ) elif msgType == fix.MsgType_MarketDataRequestReject: text = extract_message_field_value(fix.Text(), message, 'str') print(f'[fromApp] Market Data Request Reject with message: {text}') elif self.verbose: print(f'[fromApp] {sessionID} | {read_FIX_message(message)}') print('unknown message type: ', msgType)
def toAdmin(self, message, sessionID): if self.verbose: print(f'[toAdmin] {sessionID} | {read_FIX_message(message)}') log(self.logger, f'[toAdmin] {sessionID} | {read_FIX_message(message)}') msgType = fix.MsgType() message.getHeader().getField(msgType) base_str = self._client_str + ' ' + sessionID.toString() + ' | ' if msgType.getValue() == fix.MsgType_Logon: print(self._client_str + 'Sending LOGON Request') # set login credentials username = self.settings.get(sessionID).getString('Username') password = self.settings.get(sessionID).getString('Password') message.setField(fix.Username(username)) message.setField(fix.Password(password)) elif msgType.getValue() == fix.MsgType_Logout: print(self._client_str + 'Sending LOGOUT Request') elif (msgType.getValue() == fix.MsgType_Heartbeat): if self.verbose: print(self._client_str + 'Heartbeat!') else: print(f'[toAdmin] {sessionID} | {read_FIX_message(message)}')
def _update_asset(self, date_time, _symbol, depth, bid, ask, bid_size, ask_size): """ Check all fields and update data accordingly. Not all will be present in every message. """ if depth is None or _symbol != self.symbol: return new_tob_bid, new_tob_ask = False, False # replace with current value if there is no update if bid is not None: if depth < self._lowest_bid_depth or self._lowest_bid_depth == -1: self._lowest_bid_depth = depth self.BID[depth] = bid if depth == self._lowest_bid_depth: self.BID_TOB = bid new_tob_bid = True if ask is not None: if depth < self._lowest_ask_depth or self._lowest_ask_depth == -1: self._lowest_ask_depth = depth self.ASK[depth] = ask if depth == self._lowest_bid_depth: self.ASK_TOB = ask new_tob_ask = True if bid_size is not None: self.BID_SIZE[depth] = bid_size if ask_size is not None: self.ASK_SIZE[depth] = ask_size # only save complete ticks if self.BID[depth] is None or self.ASK[depth] is None or self.BID_SIZE[depth] is None or self.ASK_SIZE[depth] is None: return if self.store_all_ticks: try: self.HISTORY.append({'date_time': date_time, 'depth': depth, 'bid': self.BID[depth], 'ask': self.ASK[depth], 'bid_size': self.BID_SIZE[depth], 'ask_size': self.ASK_SIZE[depth]}) log(self.history_logger, '{},{},{},{},{},{}'.format(date_time, depth, self.BID[depth], self.ASK[depth], self.BID_SIZE[depth], self.ASK_SIZE[depth])) if new_tob_bid or new_tob_ask: self.HISTORY_TOB.append({'date_time': date_time, 'bid': self.BID_TOB, 'ask': self.ASK_TOB}) log(self.history_tob_logger, '{},{},{}'.format(date_time, self.BID_TOB, self.ASK_TOB)) except KeyError: pass
def onCreate(self, sessionID): if self.settings.get(sessionID).getString( 'SessionQualifier') == 'Quote': print('SessionQualifier=Quote, sessionID=' + sessionID.toString()) self.sender.set_sessionID_Quote(sessionID) elif self.settings.get(sessionID).getString( 'SessionQualifier') == 'Trade': print('SessionQualifier=Trade, sessionID=' + sessionID.toString()) self.sender.set_sessionID_Trade(sessionID) self.sender.set_account( self.settings.get(sessionID).getString('Account')) log(self.logger, f'Session created with sessionID = {sessionID.toString()}.')
def __init__(self, _symbol, store_all_ticks=True, save_history_to_files=True): self.symbol = _symbol self.save_history_to_files = save_history_to_files self.store_all_ticks = store_all_ticks # current bid/ask values depending on depth. top of book is self.BID[0]. self.BID = {} self.ASK = {} self.BID_SIZE = {} self.ASK_SIZE = {} self.BID_TOB = 0 self.ASK_TOB = 0 self._lowest_bid_depth = -1 self._lowest_ask_depth = -1 self.HISTORY_DIR = 'history' Path(self.HISTORY_DIR).mkdir(parents=True, exist_ok=True) self.HISTORY_FILE = f"{self.symbol.replace('/', '')}.log" self.HISTORY_FILE_TOB = f"{self.symbol.replace('/', '')}_TOB.log" self.history_logger = setup_logger('history_logger', join(self.HISTORY_DIR, self.HISTORY_FILE), '%(message)s', level=logging.INFO) self.history_tob_logger = setup_logger('history_tob_logger', join(self.HISTORY_DIR, self.HISTORY_FILE_TOB), '%(message)s', level=logging.INFO) log(self.history_logger, 'date_time,depth,bid,ask,bid_size,ask_size') log(self.history_tob_logger, 'date_time,bid,ask') self.HISTORY = [] self.HISTORY_TOB = []
def parse_ExecutionReport(self, message, sending_time): # Extract fields from the message here and pass to an upper layer if self.verbose: print('[fromApp] Execution Report received!') print(f'[fromApp] {read_FIX_message(message)}') # Tag 11 (client order ID, the one we sent) # must be the same type (int) as in open_orders. ClOrdID = extract_message_field_value(fix.ClOrdID(), message, 'int') # print('ClOrdID:', ClOrdID) # Tag 15 # _Currency = extract_message_field_value(fix.Currency(), message) # print('_Currency:', _Currency) # Tag 17 # _ExecID = extract_message_field_value(fix.ExecID(), message) # print('_ExecID:', _ExecID) # Tag 37 # _OrderID = extract_message_field_value(fix.OrderID(), message) # print('_OrderID:', _OrderID) # Tag 39 OrderStatus: 0 = New, 1 = Partially filled, 2 = Filled, 3 = Done for day, 4 = Canceled, # 6 = Pending Cancel (e.g. result of Order Cancel Request <F>), 7 = Stopped, # 8 = Rejected, 9 = Suspended, A = Pending New, B = Calculated, C = Expired, # D = Accepted for bidding, E = Pending Replace (e.g. result of Order Cancel/Replace Request <G>) # maybe also check 3=Done for day, 7=Stopped, 9=Suspended, B=Calculated and C=Expired, but it seems that Stopped means it can still be filled? # https://www.onixs.biz/fix-dictionary/4.4/tagNum_39.html ordStatus = extract_message_field_value(fix.OrdStatus(), message, 'str') # print('ordStatus:', ordStatus) # Tag 150 Execution type # 0 = New, 4 = Canceled, F = Trade (partial fill or fill), I = Order Status, ... _ExecType = extract_message_field_value(fix.ExecType(), message) # print('_ExecType:', _ExecType) # if the exection report is a response to an OrderStatusRequest, # fields other than OrdStatus might not be set. if _ExecType == 'I': if ClOrdID in self.open_orders.keys(): self.open_orders[ClOrdID].status = ordStatus else: print(f'Order {ClOrdID} not found! Order status: {ordStatus}') return # Tag 40 OrderType: 1 = Market, 2 = Limit, 3 = Stop ordType = extract_message_field_value(fix.OrdType(), message, 'str') # print('ordType:', ordType) # Tag 44 price = extract_message_field_value(fix.Price(), message, 'float') # print('price:', price) # Tag 54 side = extract_message_field_value(fix.Side(), message, 'str') # print('side:', side) # Tag 55 symbol = extract_message_field_value(fix.Symbol(), message, 'str') # print('symbol:', symbol) # canceled or rejected: here a few fields are not defined, which would # prevent further parsing of the message. therefore, exiting earlier. # https://www.onixs.biz/fix-dictionary/4.4/tagNum_39.html # tags not defined: 60, 18, 100 # [fromApp] 8=FIX.4.4, 9=164, 35=8, 34=33, 49=XCD17, 52=20201020-10:04:25.518, # 56=T008, 11=51515, 14=0.0, 17=0, 37=0, 38=1000, 39=8, 40=2, 44=1.17, 54=1, # 55=EUR/USD, 58=reject: duplicate clOrdID, 150=8, 151=0.0, 10=073 if ordStatus == '4' or ordStatus == '6' or ordStatus == '8': action = 'canceled' if ordStatus == '8': action = 'rejected' if ClOrdID in self.open_orders.keys( ): # the report might be sent after restarting the FIX algo. if ordStatus == '4' or ordStatus == '6': if self.verbose: print(f'Order {action}: {self.open_orders[ClOrdID]}') if ordStatus == '8': if self.verbose: print(f'Order {action}: {self.open_orders[ClOrdID]}') del self.open_orders[ClOrdID] elif self.verbose: print(f'Order {action}, but not found in open_orders.') report = execution_report(ClOrdID, symbol, side, price, ordType, ordStatus, 0, 0, 0, 0) self.execution_history.append(report) if self.verbose: print(report) transactTime = datetime_to_str(datetime.datetime.utcnow()) log( self.execution_logger, '{},{},{},{},{},{},{},{},{},{},{}'.format( transactTime, ClOrdID, symbol, side, price, ordType, ordStatus, 0, 0, 0, 0)) if self.read_positions_from_file: self.save_positions_to_file() self.lock.acquire() self.tick_processor.on_execution_report(report, self) self.lock.release() return # Tag 60 (how to make it a datetime object?) # here without extract_message_field_value() because we want to call getString() and not getValue(). transactTime = fix.TransactTime() message.getField(transactTime) transactTime = transactTime.getString() # print('transactTime:', transactTime) # Tag 18 orderQty = extract_message_field_value(fix.OrderQty(), message, 'int') # print('orderQty:', orderQty) # Tag 110 minQty = extract_message_field_value(fix.MinQty(), message, 'int') # print('minQty:', minQty) # Tag 14 CumQty: Total quantity filled. cumQty = extract_message_field_value(fix.CumQty(), message, 'int') # print('cumQty:', cumQty) # Tag 151 LeavesQty: Quantity open for further execution. 0 if 'Canceled', 'DoneForTheDay', # 'Expired', 'Calculated', or' Rejected', else LeavesQty <151> = OrderQty <38> - CumQty <14>. leavesQty = extract_message_field_value(fix.LeavesQty(), message, 'int') # print('leavesQty:', leavesQty) if not ClOrdID in self.open_orders.keys(): log(self.execution_logger, f'[ERROR] ClOrdID {ClOrdID} not found in open_orders:', True) for o in self.open_orders: log(self.execution_logger, o) report = execution_report(ClOrdID, symbol, side, price, ordType, ordStatus, orderQty, minQty, cumQty, leavesQty) self.execution_history.append(report) print(report) # maybe better exit if the ID was not found? # but it could happen on the start of a session with PersistMessages=Y. return self.open_orders[ClOrdID].status = ordStatus if ordStatus == '0': # new self.open_orders[ClOrdID].openTime = transactTime elif ordStatus == '1' and leavesQty > 0: # partially filled if leavesQty == 0: if ClOrdID in self.open_orders.keys(): del self.open_orders[ClOrdID] else: self.open_orders[ClOrdID].leaves_quantity = leavesQty # orderQty is the ordered quantity, cumQty the one filled. self.add_position(symbol, side, cumQty) # for consistency check. canceled quantity is orderQty-cumQty. self.add_canceled_quantity(symbol, side, orderQty - cumQty) elif ordStatus == '2': # filled if ClOrdID in self.open_orders.keys(): del self.open_orders[ClOrdID] self.add_position(symbol, side, cumQty) # if there was a partial fill before, a following canceled order can have a non-zero cumQty. elif ordStatus == '3': if ClOrdID in self.open_orders.keys(): del self.open_orders[ClOrdID] report = execution_report(ClOrdID, symbol, side, price, ordType, ordStatus, orderQty, minQty, cumQty, leavesQty) self.execution_history.append(report) if self.verbose: print(report) log( self.execution_logger, '{},{},{},{},{},{},{},{},{},{},{}'.format( transactTime, ClOrdID, symbol, side, price, ordType, ordStatus, orderQty, minQty, cumQty, leavesQty)) if self.read_positions_from_file: self.save_positions_to_file() self.lock.acquire() self.tick_processor.on_execution_report(report, self) self.lock.release()
def __init__( self, settings, tick_processor, read_positions_from_file=False, # to load positions after restart store_all_ticks=True, save_history_to_files=True, verbose=True, message_log_file='messages.log', execution_history_file='execution_history.log', client_str='[CLIENT (FIX API v4.4)] ', server_str='[SERVER (FIX API v4.4)] '): super().__init__() self.store_all_ticks = store_all_ticks self.save_history_to_files = save_history_to_files self.verbose = verbose self._position_file = 'positions.json' self._order_file = 'orders.json' self.execution_history_file = execution_history_file self.connected = False self._ID_Incrementor = 0 self.ClOrdID_Incrementor = 0 self.settings = settings self.tick_processor = tick_processor self.lock = Lock() self.sender = sender( self ) # passing self here so that we can call app functions from there. self.logger = None if len(message_log_file) > 0: self.logger = setup_logger('message_logger', message_log_file, '%(asctime)s %(levelname)s %(message)s', level=logging.INFO) self.execution_logger = None if len(execution_history_file) > 0: self.execution_logger = setup_logger('execution_logger', execution_history_file, '%(message)s', level=logging.INFO) log( self.execution_logger, 'transactTime,ClOrdID,Symbol,Side,Price,OrdType,OrdStatus,OrderQty,MinQty,CumQty,LeavesQty' ) self._client_str = client_str self._server_str = server_str # Unique identifier for Market Data Request <V> self._id_to_symbol = {} # format: '0': 'EURUSD' # Dictionary to hold Asset Histories self.history_dict = {} # format: 'EURUSD': History # Dictionary to hold open orders self.open_orders = {} # format: ClOrdID: Order self.open_net_positions = {} # format: 'EURUSD': 0.0 self.canceled_net_quantity = {} # format: 'EURUSD': 0.0 self.execution_history = [] self.read_positions_from_file = read_positions_from_file if self.read_positions_from_file: self.load_positions_from_file()
def toApp(self, message, sessionID): if self.verbose: print(f'[toApp] {sessionID} | {read_FIX_message(message)}') log(self.logger, f'[toApp] {sessionID} | {read_FIX_message(message)}')
def onMessage(self, message, sessionID): if self.verbose: print('[onMessage] {}'.format(read_FIX_message(message))) log(self.logger, f'Message = {read_FIX_message(message)}.')
def onLogout(self, sessionID): self.connected = False log(self.logger, 'Logout.') print(self._client_str + ' ' + sessionID.toString() + ' | Logout Successful')
def onLogon(self, sessionID): self.connected = True log(self.logger, 'Logon.') print(self._client_str + ' ' + sessionID.toString() + ' | Login Successful')