#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
  rtx.py
  ------

  RealTick API interface module

  Copyright (c) 2015 Reliance Systems Inc. <mkrueger@rstms.net>
  Licensed under the MIT license.  See LICENSE for details.

"""
import sys
import types
from uuid import uuid1
import ujson as json
import time
from collections import OrderedDict
from hexdump import hexdump
import pytz
import tzlocal
import datetime
import re
from pprint import pprint

from txtrader.config import Config

CALLBACK_METRIC_HISTORY_LIMIT = 1024

TIMEOUT_TYPES = ['DEFAULT', 'ACCOUNT', 'ADDSYMBOL', 'ORDER', 'ORDERSTATUS', 'POSITION', 'TIMER', 'BARCHART']

# default RealTick orders to NYSE and Stock type
RTX_EXCHANGE='NYS'
RTX_STYPE=1

# allow disable of tick requests for testing

ENABLE_CXN_DEBUG = False

DISCONNECT_SECONDS = 30 
SHUTDOWN_ON_DISCONNECT = True 

BARCHART_FIELDS = 'DISP_NAME,TRD_DATE,TRDTIM_1,OPEN_PRC,HIGH_1,LOW_1,SETTLE,ACVOL_1'
BARCHART_TOPIC = 'LIVEQUOTE'

from twisted.python import log
from twisted.python.failure import Failure
from twisted.internet.protocol import Protocol, ReconnectingClientFactory
from twisted.protocols.basic import LineReceiver 
from twisted.internet import reactor, defer
from twisted.internet.task import LoopingCall
from twisted.web import server
from socket import gethostname

class RtxClient(LineReceiver):
    delimiter = '\n'
    # set 16MB line buffer
    MAX_LENGTH = 0x1000000
    def __init__(self, rtx):
        self.rtx = rtx

    def connectionMade(self):
        self.rtx.gateway_connect(self)

    def lineReceived(self, data):
        self.rtx.gateway_receive(data)

    def lineLengthExceeded(self, line):
        self.rtx.force_disconnect('RtxClient: Line length exceeded: line=%s' % repr(line))

class RtxClientFactory(ReconnectingClientFactory):
    initialDelay = 15
    maxDelay = 60
    def __init__(self, rtx):
        self.rtx = rtx

    def startedConnecting(self, connector):
        self.rtx.output('RTGW: Started to connect.')
    
    def buildProtocol(self, addr):
        self.rtx.output('RTGW: Connected.')
        self.resetDelay()
        return RtxClient(self.rtx)

    def clientConnectionLost(self, connector, reason):
        self.rtx.output('RTGW: Lost connection.  Reason: %s' % reason)
        ReconnectingClientFactory.clientConnectionLost(self, connector, reason)
        self.rtx.gateway_connect(None)

    def clientConnectionFailed(self, connector, reason):
        self.rtx.output('Connection failed. Reason: %s' % reason)
        ReconnectingClientFactory.clientConnectionFailed(self, connector, reason)
        self.rtx.gateway_connect(None)

class API_Symbol(object):
    def __init__(self, api, symbol, client_id, init_callback):
        self.api = api
        self.api.symbols[symbol]=self
        self.id = str(uuid1())
        self.output = api.output
        self.clients = set([client_id])
        self.callback = init_callback
        self.symbol = symbol
        self.fullname = ''
        self.bid = 0.0
        self.bid_size = 0
        self.ask = 0.0
        self.ask_size = 0
        self.last = 0.0
        self.size = 0
        self.volume = 0
        self.open = 0.0
        self.close = 0.0
        self.vwap = 0.0
        self.high = 0.0
        self.low = 0.0
        self.minute_high = 0.0
        self.minute_low = 0.0
        self.last_trade_time = '00:00:00'
        self.last_trade_minute = -1
        self.last_api_minute = -1

        self.rawdata = {} 
        self.last_quote = ''
        self.output('API_Symbol %s %s created for client %s' % (self, symbol, client_id))
        self.barchart = {}

        # request initial symbol data
        self.cxn_updates = None
        self.cxn_init = api.cxn_get('TA_SRV', 'LIVEQUOTE')
        cb = API_Callback(self.api, self.cxn_init.id, 'init_symbol', RTX_LocalCallback(self.api, self.init_handler, self.init_failed), self.api.callback_timeout['ADDSYMBOL'])
        self.cxn_init.request('LIVEQUOTE', '*', "DISP_NAME='%s'" % symbol, cb)

    def __str__(self):
        return 'API_Symbol(%s bid=%s bidsize=%d ask=%s asksize=%d last=%s size=%d volume=%d close=%s vwap=%s clients=%s' % (self.symbol, self.bid, self.bid_size, self.ask, self.ask_size, self.last, self.size, self.volume, self.close, self.vwap, self.clients)

    def __repr__(self):
        return str(self)

    def export(self):
        ret = {
            'symbol': self.symbol,
            'last': self.last,
            'tradetime': self.last_trade_time,
            'size': self.size,
            'volume': self.volume,
            'open': self.open,
            'close': self.close,
            'vwap': self.vwap,
            'fullname': self.fullname,
        }
        if self.api.enable_high_low: 
          ret['high'] = self.high
          ret['low'] = self.low
        if self.api.enable_ticker:
          ret['bid'] = self.bid
          ret['bidsize'] = self.bid_size
          ret['ask'] = self.ask
          ret['asksize'] = self.ask_size
        #if self.api.enable_barchart:
        #  ret['bars'] = self.barchart_render()
        return ret

    def add_client(self, client):
        self.output('API_Symbol %s %s adding client %s' %
                    (self, self.symbol, client))
        self.clients.add(client)

    def del_client(self, client):
        self.output('API_Symbol %s %s deleting client %s' %
                    (self, self.symbol, client))
        self.clients.discard(client)
        if not self.clients:
            self.output('Removing %s from watchlist' % self.symbol)
            if self.cxn_updates:
                service, topic, table, what, where = self.quotes_advise_fields()
                cb = API_Callback(self.api, self.cxn_updates.id, 'unadvise', RTX_LocalCallback(self.api, self.cancel_quotes_advise))
                self.cxn_updates.unadvise(table, what, where, cb)
                self.cxn_updates = None

    def cancel_quotes_advise(self, data):
        self.output('quotes_advise terminated: %s' % repr(data))

    def update_quote(self):
        quote = 'quote.%s:%s %d %s %d' % (
            self.symbol, self.bid, self.bid_size, self.ask, self.ask_size)
        if quote != self.last_quote:
            self.last_quote = quote
            self.api.WriteAllClients(quote)

    def update_trade(self):
        self.api.WriteAllClients('trade.%s:%s %d %d' % (
            self.symbol, self.last, self.size, self.volume))

    def init_handler(self, data):
        data = json.loads(data)
        self.parse_fields(None, data[0])
        self.rawdata = data[0]
        for k,v in self.rawdata.items():
            if str(v).startswith('Error '):
                self.rawdata[k]=''
        self.cxn_init = None
  
        # if this is a valid symbol and barchart is enabled, request an initial chart
        if self.api.enable_barchart and not self.rawdata.get('SYMBOL_ERROR'):
 	    self.barchart_query('.', self.complete_barchart_init, self.barchart_init_failed)
        elif self.api.symbol_init(self):
            self.complete_symbol_init()

    def init_failed(self, error):
        self.api.error_handler(repr(self), 'ERROR: Initial query failed for symbol %s' % self.symbol)

    def barchart_query(self, start, callback, errback):
        self.api.query_bars(self.symbol, 1, start, '.', RTX_LocalCallback(self.api, callback, errback))
        
    def barchart_init_failed(self, error):
        self.api.error_handler(repr(self), 'ERROR: Initial BARCHART query failed for symbol %s: %s' % (self.symbol, repr(error)))

    def barchart_query_failed(self, error):
        self.api.error_handler(repr(self), 'ERROR: BARCHART query failed for symbol %s: %s' % (self.symbol, repr(error)))

    def complete_barchart_init(self, bars):
        self.barchart_update(bars)
        if self.api.symbol_init(self):
            self.complete_symbol_init()

    def complete_symbol_init(self):
        # enable live price updates 
        self.output('Adding %s to watchlist' % self.symbol)
        service, topic, table, what, where = self.quotes_advise_fields()
        self.cxn_updates = self.api.cxn_get(service, topic)
        self.cxn_updates.advise(table, what, where, self.parse_fields)

    def quotes_advise_fields(self):
        service = 'TA_SRV'
        topic = 'LIVEQUOTE'
        table = 'LIVEQUOTE'
        what = 'TRD_DATE,TRDTIM_1,TRDPRC_1,TRDVOL_1,ACVOL_1,OPEN_PRC,HST_CLOSE,VWAP'
        if self.api.enable_ticker:
            what += ',BID,BIDSIZE,ASK,ASKSIZE'
        if self.api.enable_high_low:
            what += ',HIGH_1,LOW_1'
        where = "DISP_NAME='%s'" % self.symbol
        return (service, topic, table, what, where)
        
    def parse_fields(self, cxn, data):
        trade_flag = False
        quote_flag = False
        pid = 'API_Symbol(%s)' % self.symbol
 
        if data == None:
            self.api.force_disconnect('LIVEQUOTE Advise has been terminated by API for %s' % pid)
            return

        if 'TRDPRC_1' in data.keys():
            self.last = self.api.parse_tql_float(data['TRDPRC_1'], pid, 'TRDPRC_1')
            trade_flag = True
            if 'TRDTIM_1' in data.keys() and 'TRD_DATE' in data.keys():
                self.last_trade_time = ' '.join(self.api.format_barchart_date(data['TRD_DATE'], data['TRDTIM_1'], pid))
            else:
                self.api.error_handler(repr(self), 'ERROR: TRDPRC_1 without TRD_DATE, TRDTIM_1')
           
            # don't request an update during the symbol init processing
            if self.api.enable_barchart and (not self.cxn_init):
                # query a barchart update after each trade
 	        self.barchart_query('-5', self.barchart_update, self.barchart_query_failed)

        if 'HIGH_1' in data.keys():
            self.high = self.api.parse_tql_float(data['HIGH_1'], pid, 'HIGH_1')
            trade_flag = True
        if 'LOW_1' in data.keys():
            self.low = self.api.parse_tql_float(data['LOW_1'], pid, 'LOW_1')
            trade_flag = True
        if 'TRDVOL_1' in data.keys():
            self.size = self.api.parse_tql_int(data['TRDVOL_1'], pid, 'TRDVOL_1')
            trade_flag = True
        if 'ACVOL_1' in data.keys():
            self.volume = self.api.parse_tql_int(data['ACVOL_1'], pid, 'ACVOL_1')
            trade_flag = True
        if 'BID' in data.keys():
            self.bid = self.api.parse_tql_float(data['BID'], pid, 'BID')
            if self.bid and 'BIDSIZE' in data.keys():
                self.bidsize = self.api.parse_tql_int(data['BIDSIZE'], pid, 'BIDSIZE')
            else:
                self.bidsize = 0
            quote_flag = True
        if 'ASK' in data.keys():
            self.ask = self.api.parse_tql_float(data['ASK'], pid, 'ASK')
            if self.ask and 'ASKSIZE' in data.keys():
              self.asksize = self.api.parse_tql_int(data['ASKSIZE'], pid, 'ASKSIZE')
            else:
                self.asksize = 0
            quote_flag = True
        if 'COMPANY_NAME' in data.keys():
            self.fullname = self.api.parse_tql_str(data['COMPANY_NAME'], pid, 'COMPANY_NAME')
        if 'OPEN_PRC' in data.keys():
            self.open = self.api.parse_tql_float(data['OPEN_PRC'], pid, 'OPEN_PRC')
        if 'HST_CLOSE' in data.keys():
            self.close = self.api.parse_tql_float(data['HST_CLOSE'], pid, 'HST_CLOSE')
        if 'VWAP' in data.keys():
            self.vwap = self.api.parse_tql_float(data['VWAP'], pid, 'VWAP')

        if self.api.enable_ticker:
            if quote_flag:
                self.update_quote()
            if trade_flag:
                self.update_trade()

    def barchart_render(self):
        return [key.split(' ') + self.barchart[key] for key in sorted(self.barchart.keys())]

    def barchart_update(self, bardata):
        bars_found=False
        if bardata:
            bars = json.loads(bardata)
            if bars:
                for bar in bars:
                    self.barchart['%s %s' % (bar[0], bar[1])] = bar[2:]        
                    bars_found=True
        if not bars_found:
            self.api.error_handler(self.symbol, 'barchart_update: no bars found in %s' % repr(bardata))



class API_Order(object):
    def __init__(self, api, oid, data, origin, callback=None):
        #pprint({'new API_Order id=%s origin=%s' % (oid, origin): data})
        self.api = api
        self.oid = oid
        self.callback = callback
        self.updates = []
        self.suborders = {}
        self.fields = {}
        self.identified = False
        self.ticket = 'undefined'
        data['status'] = 'Initialized'
        data['origin'] = origin
        self.update(data, init=True)

    def identify_order_type(self, data):
        if not self.identified:
            if 'TYPE' in data:
                otype = data['TYPE']
                # set ticket flag based on first TYPE encountered
                self.ticket = 'ticket' if otype.startswith('UserSubmitStaged') else 'order'
                self.fields['type'] = otype
                self.identified = True

    def initial_update(self, data):
        self.update(data)
        if self.callback:
            self.callback.complete(self.render())
            self.callback = None

    def update(self, data, init=False):

        field_state = json.dumps(self.fields)

        self.identify_order_type(data)
    
        if 'ORDER_ID' in data:
            order_id = data['ORDER_ID']
            if order_id in self.suborders.keys():
                if data == self.suborders[order_id]:
                    change = 'dup'
                else:
                     change = 'changed'
            else:
                change = 'new'
            self.suborders[order_id] = data
        else:
            if init:
                order_id = '(init)'
                change = 'new'
            else:
                self.api.error_handler(self.oid, 'Order Update without ORDER_ID: %s' % repr(data))
                order_id = 'unknown'
                change = 'error'

        if self.api.log_order_updates:
            self.api.output('ORDER_UPDATE: OID=%s ORDER_ID=%s %s' % (self.oid, order_id, change))

        # only apply new or changed messages to the base order; (don't move order status back in time when refresh happens)

        if change in ['new', 'changed']:
            changes={} 
            for k,v in data.items():
                ov = self.fields.setdefault(k,None)
                self.fields[k]=v
                if v!=ov:
                    changes[k]=v

            if changes:
                if self.api.log_order_updates:
                    self.api.output('ORDER_CHANGES: OID=%s ORDER_ID=%s %s' % (self.oid, order_id, repr(changes)))
                #if order_id != self.oid:
                update_type = data['TYPE'] if 'TYPE' in data else 'Undefined'
                self.updates.append({'id': order_id, 'type':  update_type, 'fields': changes, 'time': time.time() })

        if not init:
            if json.dumps(self.fields) != field_state:
                self.api.send_order_status(self)

    def update_fill_fields(self):
        if self.fields['TYPE'] in ['UserSubmitOrder', 'ExchangeTradeOrder']:
            if 'VOLUME_TRADED' in self.fields:
                self.fields['filled'] =self.fields['VOLUME_TRADED']
            if 'ORDER_RESIDUAL' in self.fields:
                self.fields['remaining']=self.fields['ORDER_RESIDUAL']
            if 'AVG_PRICE' in self.fields: 
                self.fields['avgfillprice']=self.fields['AVG_PRICE']

    def render(self):
        # customize fields for standard txTrader order status 
        if 'ORIGINAL_ORDER_ID' in self.fields:
            self.fields['permid']=self.fields['ORIGINAL_ORDER_ID']
        self.fields['symbol']=self.fields['DISP_NAME']
        self.fields['account']=self.api.make_account(self.fields)
        self.fields['quantity']=self.fields['VOLUME']
        self.fields['class'] = self.ticket

        status = self.fields.setdefault('CURRENT_STATUS', 'UNDEFINED')
        otype = self.fields.setdefault('TYPE', 'Undefined')
        #print('render: permid=%s ORDER_ID=%s CURRENT_STATUS=%s TYPE=%s' % (self.fields['permid'], self.fields['ORDER_ID'], status, otype))
        #pprint(self.fields)
        if status=='PENDING': 
            self.fields['status'] = 'Submitted'
        elif status=='LIVE':
            self.fields['status'] = 'Pending'
            self.update_fill_fields()
        elif status=='COMPLETED':
            if self.is_filled():
                self.fields['status'] = 'Filled'
                if otype == 'ExchangeTradeOrder':
                    self.update_fill_fields()
            elif otype in ['UserSubmitOrder', 'UserSubmitStagedOrder', 'UserSubmitStatus', 'ExchangeReportStatus']:
                self.fields['status'] = 'Submitted'
                self.update_fill_fields()
            elif otype == 'UserSubmitCancel':
                self.fields['status'] = 'Cancelled'
            elif otype in ['UserSubmitChange', 'AdjustQty']:
                self.fields['status'] = 'Changed'
            elif otype == 'ExchangeAcceptOrder':
                self.fields['status'] = 'Accepted'
            elif otype == 'ExchangeTradeOrder':
                self.update_fill_fields()
            elif otype in ['ClerkReject', 'ExchangeKillOrder']:
                self.fields['status'] = 'Error'
            else:
                self.api.error_handler(self.oid, 'Unknown TYPE: %s' % otype)
                self.fields['status'] = 'Error'
        elif status=='CANCELLED':
            self.fields['status'] = 'Cancelled'
        elif status=='DELETED':
            self.fields['status'] = 'Error'
        else:
            self.api.error_handler(self.oid, 'Unknown CURRENT_STATUS: %s' % status)
            self.fields['status'] = 'Error'
            
        self.fields['updates'] = self.updates
        f = self.fields 
        self.fields['text'] = '%s %d %s (%s)' % (f['BUYORSELL'], int(f['quantity']), f['symbol'], f['status'])

        ret = {'raw':{}}
        for k,v in self.fields.iteritems():
            if k.islower():
                ret[k]=v
            else:
                ret['raw'][k]=v
        return ret

    def is_filled(self):
        return bool(self.fields['CURRENT_STATUS']=='COMPLETED' and
            self.has_fill_type() and
            'ORIGINAL_VOLUME' in self.fields and
            'VOLUME_TRADED' in self.fields and 
            self.fields['ORIGINAL_VOLUME'] == self.fields['VOLUME_TRADED'])

    def is_cancelled(self):
        return bool(self.fields['CURRENT_STATUS']=='COMPLETED' and
            'status' in self.fields and self.fields['status'] == 'Error' and
            'REASON' in self.fields and self.fields['REASON'] == 'User cancel')
 
    def has_fill_type(self):
        if self.fields['TYPE']=='ExchangeTradeOrder':
            return True
        for update_type in [update['type'] for update in self.updates]:
            if update_type =='ExchangeTradeOrder':
                return True
        return False

class API_Callback(object):
    def __init__(self, api, id, label, callable, timeout=0):
        """callable is stored and used to return results later"""
        #api.output('API_Callback.__init__%s' % repr((self, api, id, label, callable, timeout)))
        self.api = api
        self.id = id
        self.label = label
        self.started = time.time()
        self.timeout = timeout or api.callback_timeout['DEFAULT']
        self.expire = self.started + timeout
        self.callable = callable
        self.done = False
        self.data = None
        self.expired = False

    def complete(self, results):
        """complete callback by calling callable function with value of results"""
        self.elapsed = time.time() - self.started
        if not self.done:
            ret = self.format_results(results)
            if self.callable.callback.__name__ == 'sendString':
                ret = '%s.%s: %s' % (self.api.channel, self.label, ret)
            #self.api.output('API_Callback.complete(%s)' % repr(ret))
            self.callable.callback(ret)
            self.callable = None
            self.done = True
        else:
            self.api.error_handler(self.id, '%s completed after timeout: callback=%s elapsed=%.2f' % (self.label, repr(self), self.elapsed))
            self.api.output('results=%s' % repr(results))
        self.api.record_callback_metrics(self.label, int(self.elapsed * 1000), self.expired)

    def check_expire(self):
        #SElf.api.output('API_Callback.check_expire() %s' % self)
        if not self.done:
            if time.time() > self.expire:
                msg = 'error: callback expired: %s' % repr((self.id, self.label, self))
                self.api.WriteAllClients(msg)
                if self.callable.callback.__name__ == 'sendString':
                    self.callable.callback('%s.error: %s callback expired', (self.api.channel, self.label))
                else:
                    self.callable.errback(Failure(Exception(msg)))
                self.expired = True
                self.done = True

    # TODO: all of these format_* really belong in the api class

    def format_results(self, results):
        #print('format_results: label=%s results=%s' % (self.label, results))
        if self.label == 'account_data':
            results = self.format_account_data(results)
        elif self.label == 'positions':
            results = self.format_positions(results)
        elif self.label == 'orders':
            results = self.format_orders(results)
        elif self.label == 'tickets':
            results = self.format_tickets(results)
        elif self.label=='executions':
            results = self.format_executions(results)
        elif self.label == 'order_status':
            results = self.format_orders(results, self.id)
        elif self.label == 'barchart':
            results = self.api.format_barchart(results)

        return json.dumps(results)

    def format_account_data(self, rows):
        data = rows[0] if rows else rows
        if data and 'EXCESS_EQ' in data:
            data['_cash'] = round(float(data['EXCESS_EQ']),2)
        return data

    def format_positions(self, rows):
        # Positions should return {'ACCOUNT': {'SYMBOL': QUANTITY, ...}, ...}
        positions = {}
        [positions.setdefault(a, {}) for a in self.api.accounts]
	#print('format_positions: rows=%s' % repr(rows))
        for pos in rows:
            if pos:
	        #print('format_positions: pos=%s' % repr(pos))
                account = self.api.make_account(pos)
                symbol = pos['DISP_NAME']
                positions[account].setdefault(symbol, 0)
                # if LONG positions exist, add them, if SHORT positions exist, subtract them
                for m,f in [(1,'LONGPOS'), (1, 'LONGPOS0'), (-1, 'SHORTPOS'), (-1, 'SHORTPOS0')]:
                    if f in pos:
                        positions[account][symbol] += m * int(pos[f])
        return positions

    def format_orders(self, rows, oid=None):
        return self._format_orders(rows, oid, 'order')

    def format_tickets(self, rows, oid=None):
        return self._format_orders(rows, oid, 'ticket')

    def _format_orders(self, rows, oid, _filter):
        #pprint({'format_orders': rows})
        #print('_format_orders %s %s' % (oid, _filter))
        for row in rows or []:
            if row:
                self.api.handle_order_response(row)
        if oid:
            results = self.api.orders[oid].render() if oid in self.api.orders else None
        else:
            results={}
            for k,v in self.api.orders.items():
                # don't return staged order tickets
                if v.ticket == _filter:
                    results[k] = v.render()
        return results

    def format_executions(self, rows):
        for row in rows:
            if row:
                self.api.handle_order_response(row)
        results={}
        for k,v in self.api.orders.items():
            if v.is_filled():
                results[k]=v.fields
                results[k]['updates']=v.updates
        return results


class RTX_Connection(object):
    def __init__(self, api, service, topic, enable_logging=False):
        self.api = api
        self.id = str(uuid1())
        self.service = service
        self.topic = topic
        self.key = '%s;%s' % (service, topic)
        self.last_query = ''
        self.api.output('RTX_Connection init %s' % repr(self))
        self.api.cxn_register(self)
        self.api.gateway_send('connect %s %s' % (self.id, self.key))
        self.ack_pending = 'CONNECTION PENDING'
        self.log = enable_logging
        self.ack_callback = None
        self.response_pending = None
        self.response_callback = None
        self.response_rows = None
        self.status_pending = 'OnInitAck'
        self.status_callback = None
        self.update_callback = None
        self.update_handler = None
        self.connected = False
        self.on_connect_action = None
        self.update_ready()

    def __del__(self):
        self.api.output('RTX_Connection delete %s' % repr(self))

    def __repr__(self):
        return self.__str__()

    def __str__(self):
        return '<RTX_Connection instance at %s %s %s %s>' % (hex(id(self)), self.id, self.key, self.last_query)

    def update_ready(self):
        self.ready = not(
            self.ack_pending or self.response_pending or self.status_pending or self.status_callback or self.update_callback or self.update_handler)
        #self.api.output('update_ready() %s %s' % (self.id, self.ready))
        if self.ready:
            self.api.cxn_activate(self)

    def receive(self, _type, data):
        if _type == 'ack':
            self.handle_ack(data)
        elif _type == 'response':
            self.handle_response(data)
        elif _type == 'status':
            self.handle_status(data)
        elif _type == 'update':
            self.handle_update(data)
        else:
            self.api.error_handler(self.id, 'Message Type Unexpected: %s' % data)
        self.update_ready()

    def handle_ack(self, data):
        if self.log:
            self.api.output('Ack Received: %s %s' % (self.id, data))
        if self.ack_pending:
            if data == self.ack_pending:
                self.ack_pending = None
            else:
                self.api.error_handler(self.id, 'Ack Mismatch: expected %s, got %s' % (self.ack_pending, data))
                self.handle_response_failure()
            if self.ack_callback:
                self.ack_callback.complete(data)
                self.ack_callback = None
        else:
            self.api.error_handler(self.id, 'Ack Unexpected: %s' % data)

    def handle_response(self, data):
        if self.log:
            self.api.output('Connection Response: %s %s' % (self, data))
        if self.response_pending:
            self.response_rows.append(data['row'])
            if data['complete']:
                if self.response_callback:
                    self.response_callback.complete(self.response_rows)
                    self.response_callback = None
                self.response_pending = None
                self.response_rows = None
        else:
            self.api.error_handler(id, 'Response Unexpected: %s' % data)

    def handle_response_failure(self):
        if self.response_callback:
            self.response_callback.complete(None)

    def handle_status(self, data):
        if self.log:
            self.api.output('Connection Status: %s %s' % (self, data))
        if self.status_pending and data['msg'] == self.status_pending:
            # if update_handler is set (an Advise is active) then leave status_pending, because we'll 
            # get sporadic OnOtherAck status messages mixed in with the update messages
            # in all other cases, clear status_pending, since we only expect the one status message
            if not self.update_handler:
                self.status_pending = None

            if data['status'] == '1':
                # special case for the first status ack of a new connection; we may need to do on_connect_action
                if data['msg'] == 'OnInitAck':
                    self.connected = True
                    if self.on_connect_action:
                        self.ready = True
                        cmd, arg, exa, cba, cbr, exs, cbs, cbu, uhr = self.on_connect_action
                        self.api.output('%s sending on_connect_action: %s' % (repr(self), repr(self.on_connect_action)))
                        self.send(cmd, arg, exa, cba, cbr, exs, cbs, cbu, uhr)
                        self.on_connect_action = None
                        self.api.output('after on_connect_action send: self.status_pending=%s' % self.status_pending)

                if self.status_callback:
                    self.status_callback.complete(data)
                    self.status_callback = None
            else:
                self.api.error_handler(self.id, 'Status Error: %s' % data)
        else:
            self.api.error_handler(self.id, 'Status Unexpected: %s' % data)
            # if ADVISE is active; call handler function with None to notifiy caller the advise has been terminated
            if self.update_handler and data['msg']=='OnTerminate':
                self.update_handler(self, None)
            self.handle_response_failure()

    def handle_update(self, data):
        if self.log:
            self.api.output('Connection Update: %s %s' % (self, repr(d)))
        if self.update_callback:
            self.update_callback.complete(data['row'])
            self.update_callback = None
        else:
            if self.update_handler:
                self.update_handler(self, data['row'])
            else:
                self.api.error_handler(self.id, 'Update Unexpected: %s' % repr(data))

    def query(self, cmd, table, what, where, expect_ack=None, ack_callback=None, response_callback=None, expect_status=None, status_callback=None, update_callback=None, update_handler=None):
        tql='%s;%s;%s' % (table, what, where)
        self.last_query='%s: %s' % (cmd, tql)
        ret = self.send(cmd, tql, expect_ack, ack_callback, response_callback, expect_status, status_callback, update_callback, update_handler)

    def request(self, table, what, where, callback):
        return self.query('request', table, what, where, expect_ack='REQUEST_OK', response_callback=callback)

    def advise(self, table, what, where, handler):
        return self.query('advise', table, what, where, expect_ack='ADVISE_OK', expect_status='OnOtherAck', update_handler=handler)

    def adviserequest(self, table, what, where, callback, handler):
        return self.query('adviserequest', table, what, where, expect_ack='ADVISE_REQUEST_OK', response_callback=callback, expect_status='OnOtherAck', update_handler=handler)

    def unadvise(self, table, what, where, callback):
        # force ready state so the unadvise command will be sent
        self.ready = True
        return self.query('unadvise', table, what, where, expect_ack='UNADVISE_OK', expect_status='OnOtherAck', status_callback=callback)

    def poke(self, table, what, where, data, ack_callback, callback):
        tql = '%s;%s;%s!%s' % (table, what, where, data)
        self.last_query = 'poke: %s' % tql
        return self.send('poke', tql, expect_ack="POKE_OK", ack_callback=ack_callback, expect_status='OnOtherAck', status_callback=callback)

    def execute(self, command, callback):
        self.last_query = 'execute: %s' % command
        return self.send('execute', command, expect_ack="EXECUTE_OK", ack_callback=callback)

    def terminate(self, code, callback):
        self.last_query = 'terminate: %s' % str(code) 
        return self.send('terminate', str(code), expect_ack="TERMINATE_OK", ack_callback=callback)

    def send(self, cmd, args, expect_ack=None, ack_callback=None, response_callback=None, expect_status=None, status_callback=None, update_callback=None, update_handler=None):
        if self.ready:
            self.cmd = cmd
            if 'request' in cmd:
                self.response_rows = []
            ret = self.api.gateway_send('%s %s %s' % (cmd, self.id, args))
            self.ack_pending = expect_ack
            self.ack_callback = ack_callback
            self.response_pending = bool(response_callback)
            self.response_callback = response_callback
            self.status_pending = expect_status
            self.status_callback = status_callback
            self.update_callback = update_callback
            self.update_handler = update_handler
        else:
            if self.on_connect_action:
                self.api.error_handler(self.id, 'Failure: on_connect_action already exists: %s' % repr(self.on_connect_action))
                ret = False
            else:
                self.api.output('%s storing on_connect_action (%s)...' % (self, cmd))
                self.on_connect_action = (cmd, args, expect_ack, ack_callback, response_callback, expect_status, status_callback, update_callback, update_handler)
                ret = True
        return ret


class RTX_LocalCallback(object):
    def __init__(self, api, callback_handler, errback_handler=None):
        self.api = api
        self.callable = callback_handler
        self.errback_handler = errback_handler

    def callback(self, data):
        if self.callable:
            self.callable(data)
        else:
            self.api.error_handler(repr(self), 'Failure: undefined callback_handler for Connection: %s data=%s' % (repr(self), repr(data)))

    def errback(self, error):
        if self.errback_handler:
            self.errback_handler(error)
        else:
            self.api.error_handler(repr(self), 'Failure: undefined errback_handler for Connection: %s error=%s' % (repr(self), repr(error)))

class RTX(object):
    def __init__(self):
        self.label = 'RTX Gateway'
        self.channel = 'rtx'
        self.id = 'RTX'
        self.output('RTX init')
        self.config = Config(self.channel)
        self.api_hostname = self.config.get('API_HOST')
        self.api_port = int(self.config.get('API_PORT'))
        self.username = self.config.get('USERNAME')
        self.password = self.config.get('PASSWORD')
        self.http_port = int(self.config.get('HTTP_PORT'))
        self.tcp_port = int(self.config.get('TCP_PORT'))
        self.enable_ticker = bool(int(self.config.get('ENABLE_TICKER')))
        self.enable_high_low= bool(int(self.config.get('ENABLE_HIGH_LOW')))
        self.enable_barchart = bool(int(self.config.get('ENABLE_BARCHART')))
        self.enable_seconds_tick = bool(int(self.config.get('ENABLE_SECONDS_TICK')))
        self.log_api_messages = bool(int(self.config.get('LOG_API_MESSAGES')))
        self.debug_api_messages = bool(int(self.config.get('DEBUG_API_MESSAGES')))
        self.log_client_messages = bool(int(self.config.get('LOG_CLIENT_MESSAGES')))
        self.log_order_updates = bool(int(self.config.get('LOG_ORDER_UPDATES')))
        self.time_offset = int(self.config.get('TIME_OFFSET'))
        self.clients = set([])
        if self.time_offset:
            if not 'test' in gethostname():
                self.error_handler(self.id, 'TIME_OFFSET disallowed outside of test mode; resetting to 0')
                self.time_offset=0
        self.callback_timeout = {}
        for t in TIMEOUT_TYPES:
            self.callback_timeout[t] = int(self.config.get('TIMEOUT_%s' % t))
            self.output('callback_timeout[%s] = %d' % (t, self.callback_timeout[t]))
        self.now = None
        self.feed_now = None
        self.trade_minute = -1
        self.feedzone = pytz.timezone(self.config.get('API_TIMEZONE'))
        self.localzone = tzlocal.get_localzone()
        self.current_account = ''
        self.orders = {}
        self.pending_orders = {}
        self.tickets = {}
        self.pending_tickets = {}
        self.openorder_callbacks = []
        self.accounts = None
        self.account_data = {}
        self.pending_account_data_requests = set([])
        self.positions = {}
        self.position_callbacks = []
        self.executions = {}
        self.execution_callbacks = []
        self.order_callbacks = []
        self.bardata_callbacks = []
        self.cancel_callbacks = []
        self.order_status_callbacks = []
        self.ticket_callbacks = []
        self.add_symbol_callbacks = []
        self.accountdata_callbacks = []
        self.set_account_callbacks = []
        self.account_request_callbacks = []
        self.account_request_pending = True
        self.timer_callbacks = []
        self.connected = False
        self.last_connection_status = ''
        self.connection_status = 'Initializing'
        self.LastError = -1
        self.next_order_id = -1
        self.last_minute = -1
        self.symbols = {}
        self.barchart = None
        self.primary_exchange_map = {}
        self.gateway_sender = None
        self.active_cxn = {}
        self.idle_cxn = {}
        self.cx_time = None
        self.seconds_disconnected = 0
        self.callback_metrics = {}
        self.set_order_route(self.config.get('API_ROUTE'), None)
        reactor.connectTCP(self.api_hostname, self.api_port, RtxClientFactory(self))
        self.repeater = LoopingCall(self.EverySecond)
        self.repeater.start(1)

    def flags(self):
        return {
          'TICKER': self.enable_ticker,
          'HIGH_LOW': self.enable_high_low,
          'BARCHART': self.enable_barchart,
          'SECONDS_TICK': self.enable_seconds_tick,
          'TIME_OFFSET': self.time_offset,
        }

    def record_callback_metrics(self, label, elapsed, expired):
        m = self.callback_metrics.setdefault(label, {'tot':0, 'min': 9999, 'max': 0, 'avg': 0, 'exp': 0, 'hst': []})
        total = m['tot']  
        m['tot'] += 1
        m['min'] = min(m['min'], elapsed)
        m['max'] = max(m['max'], elapsed)
        m['avg'] = (m['avg'] * total + elapsed) / (total + 1)
        m['exp'] += int(expired)
        m['hst'].append(elapsed)
        if len(m['hst']) > CALLBACK_METRIC_HISTORY_LIMIT:
          del m['hst'][0]
        
    def cxn_register(self, cxn):
        if ENABLE_CXN_DEBUG:
            self.output('cxn_register: %s' % repr(cxn))
        self.active_cxn[cxn.id] = cxn

    def cxn_activate(self, cxn):
        if ENABLE_CXN_DEBUG:
            self.output('cxn_activate: %s' % repr(cxn))
        if not cxn.key in self.idle_cxn.keys():
            self.idle_cxn[cxn.key] = []
        self.idle_cxn[cxn.key].append(cxn)

    def cxn_get(self, service, topic):
        key = '%s;%s' % (service, topic)
        if key in self.idle_cxn.keys() and len(self.idle_cxn[key]):
            cxn = self.idle_cxn[key].pop()
        else:
            cxn = RTX_Connection(self, service, topic)
        if ENABLE_CXN_DEBUG:
            self.output('cxn_get() returning: %s' % repr(cxn))
        return cxn

    def gateway_connect(self, protocol):
        if protocol:
            self.gateway_sender = protocol.sendLine
            self.gateway_transport = protocol.transport
            self.update_connection_status('Connecting')
        else:
            self.gateway_sender = None
            self.connected = False
            self.seconds_disconnected = 0
            self.account_request_pending = False
            self.accounts = None
            self.update_connection_status('Disconnected')
            self.error_handler(self.id, 'error: API Disconnected')

        return self.gateway_receive

    def gateway_send(self, msg):
        if self.debug_api_messages:
            self.output('<--TX[%d]--' % (len(msg)))
            hexdump(msg)
        if self.log_api_messages:
            self.output('<-- %s' % repr(msg))
        if self.gateway_sender:
            self.gateway_sender('%s\n' % str(msg))

    def dump_input_message(self, msg):
        self.output('--RX[%d]-->' % (len(msg)))
        hexdump(msg)

    def receive_exception(self, t, e, msg):
        self.error_handler(self.id, 'Exception %s %s parsing data from RTGW' % (t, e))
        self.dump_input_message(msg)
        return None

    def gateway_receive(self, msg):
        """handle input from rtgw """

        if self.debug_api_messages:
            self.dump_input_message(msg)

        try:
            o = json.loads(msg)
        except Exception as e:
            return self.receive_exception(sys.exc_info()[0], e, msg)

        msg_type = o['type']
        msg_id = o['id']
        msg_data = o['data']

        if self.log_api_messages:
            self.output('--> %s %s %s' % (msg_type, msg_id, msg_data))

        if msg_type == 'system':
            self.handle_system_message(msg_id, msg_data)
        else:
            if msg_id in self.active_cxn.keys():
                c = self.active_cxn[msg_id].receive(msg_type, msg_data)
            else:
                self.error_handler(self.id, 'Message Received on Unknown connection: %s' % repr(msg))

        return True

    def handle_system_message(self, id, data):
        if data['msg'] == 'startup':
            self.connected = True
            self.accounts = None
            self.update_connection_status('Startup')
            self.output('Connected to %s' % data['item'])
            self.setup_local_queries()
        else:
            self.error_handler(self.id, 'Unknown system message: %s' % repr(data))

    def setup_local_queries(self):
        """Upon connection to rtgw, start automatic queries"""
        #what='BANK,BRANCH,CUSTOMER,DEPOSIT'
        what='*'
        self.rtx_request('ACCOUNT_GATEWAY', 'ORDER', 'ACCOUNT', what, '',
                         'accounts', self.handle_accounts, self.accountdata_callbacks, self.callback_timeout['ACCOUNT'],
                         self.handle_initial_account_failure)

        self.cxn_get('ACCOUNT_GATEWAY', 'ORDER').advise('ORDERS', '*', '', self.handle_order_update)
        
        self.rtx_request('ACCOUNT_GATEWAY', 'ORDER', 'ORDERS', '*', '',
                        'orders', self.handle_initial_orders_response, self.openorder_callbacks, self.callback_timeout['ORDERSTATUS'])

    def handle_initial_account_failure(self, message):
        self.force_disconnect('Initial account query failed (%s)' % repr(message))

    def handle_initial_orders_response(self, rows):
        self.output('Initial Orders refresh complete.')

    def output(self, msg):
        if 'error' in msg:
            log.err(msg)
        else:
            log.msg(msg)

    def open_client(self, client):
        self.clients.add(client)

    def close_client(self, client):
        self.clients.discard(client)
        symbols = self.symbols.values()
        for ts in symbols:
            if client in ts.clients:
                ts.del_client(client)
                if not ts.clients:
                    del(self.symbols[ts.symbol])

    def set_primary_exchange(self, symbol, exchange):
        if exchange:
            self.primary_exchange_map[symbol] = exchange
        else:
            del(self.primary_exchange_map[symbol])
        return self.primary_exchange_map

    def CheckPendingResults(self):
        # check each callback list for timeouts
        for cblist in [self.timer_callbacks, self.position_callbacks, self.ticket_callbacks, self.openorder_callbacks, self.execution_callbacks, self.bardata_callbacks, self.order_callbacks, self.cancel_callbacks, self.add_symbol_callbacks, self.accountdata_callbacks, self.set_account_callbacks, self.account_request_callbacks, self.order_status_callbacks]:
            dlist = []
            for cb in cblist:
                cb.check_expire()
                if cb.done:
                    dlist.append(cb)
            # delete any callbacks that are done
            for cb in dlist:
                cblist.remove(cb)

    def handle_order_update(self, cxn, msg):
        if msg:
          self.handle_order_response(msg)
        else:
          self.force_disconnect('API Order Status ADVISE connection has been terminated; connection has failed')

    def handle_order_response(self, msg):
        #print('---handle_order_response: %s' % repr(msg))
        oid = msg['ORIGINAL_ORDER_ID'] if 'ORIGINAL_ORDER_ID' in msg else None
        ret = None
        if oid:
            if self.pending_orders and 'CLIENT_ORDER_ID' in msg:
                # this is a newly created order, it has a CLIENT_ORDER_ID
                coid = msg['CLIENT_ORDER_ID']
                if coid in self.pending_orders:
                    self.pending_orders[coid].initial_update(msg)
                    self.orders[oid] = self.pending_orders[coid]
                    del self.pending_orders[coid]
            elif self.pending_orders and (oid in self.pending_orders.keys()):
                # this is a change order, ORIGINAL_ORDER_ID will be a key in pending_orders
                self.pending_orders[oid].initial_update(msg)
                del self.pending_orders[oid]
            elif oid in self.orders.keys():
                # this is an existing order, so update it
                self.orders[oid].update(msg)
            else:
                # we've never seen this order, so add it to the collection and update it
                o = API_Order(self, oid, msg, 'realtick')
                self.orders[oid]=o
                o.update(msg)
        else:
            self.error_handler(self.id, 'handle_order_update: ORIGINAL_ORDER_ID not found in %s' % repr(msg))
            #self.output('error: handle_order_update: ORIGINAL_ORDER_ID not found in %s' % repr(msg))

    def handle_ticket_update(self, cxn, msg):
        return self.handle_ticket_response(msg)

    def handle_ticket_response(self, msg):
        tid = msg['CLIENT_ORDER_ID'] if 'CLIENT_ORDER_ID' in msg else None
        if self.pending_tickets and tid in self.pending_tickets.keys():
            self.pending_tickets[tid].initial_update(msg)
            self.tickets[tid] = self.pending_tickets[tid]
            del self.pending_tickets[tid]

    def send_order_status(self, order):
        fields = order.render()
        self.WriteAllClients('%s.%s %s %s %s' % (order.ticket, fields['permid'], fields['account'], fields['raw']['TYPE'], fields['status']))

    def make_account(self, row):
        return '%s.%s.%s.%s' % (row['BANK'], row['BRANCH'], row['CUSTOMER'], row['DEPOSIT'])

    def handle_accounts(self, rows):
        rows = json.loads(rows)
        if rows:
            self.accounts = list(set([self.make_account(row) for row in rows]))
            self.accounts.sort()
            self.account_request_pending = False
            self.WriteAllClients('accounts: %s' % json.dumps(self.accounts))
            self.update_connection_status('Up')
            for cb in self.account_request_callbacks:
                cb.complete(self.accounts)

            for cb in self.set_account_callbacks:
                self.output('set_account: processing deferred response.')
                self.process_set_account(cb.id, cb)
        else:
            self.handle_initial_account_failure('initial account query returned no data')

    def set_account(self, account_name, callback):
        cb = API_Callback(self, account_name, 'set-account', callback)
        if self.accounts:
            self.process_set_account(account_name, cb)
        elif self.account_request_pending:
            self.set_account_callbacks.append(cb)
        else:
            self.error_handler(self.id, 'set_account; no data, but no account_request_pending')
            cb.complete(None)

    def verify_account(self, account_name):
        if account_name in self.accounts:
            ret = True
        else:
            msg = 'account %s not found' % account_name
            self.error_handler(self.id, 'set_account(): %s' % msg)
            ret = False
        return ret

    def process_set_account(self, account_name, callback):
        ret = self.verify_account(account_name)
        if ret:
            self.current_account = account_name
            self.WriteAllClients('current-account: %s' % self.current_account)

        if callback:
            callback.complete(ret)
        else:
            return ret

    def rtx_request(self, service, topic, table, what, where, label, handler, cb_list, timeout, error_handler=None):
        cxn = self.cxn_get(service, topic)
        cb = API_Callback(self, cxn.id, label, RTX_LocalCallback(self, handler, error_handler), timeout)
        cxn.request(table, what, where, cb)
        cb_list.append(cb)

    def EverySecond(self):
        if self.connected:
            if self.enable_seconds_tick:
                self.rtx_request('TA_SRV', 'LIVEQUOTE', 'LIVEQUOTE', 'DISP_NAME,TRDTIM_1,TRD_DATE',
                                 "DISP_NAME='$TIME'", 'tick', self.handle_time, self.timer_callbacks, 
                                 self.callback_timeout['TIMER'], self.handle_time_error)
        else:
            self.seconds_disconnected += 1
            if self.seconds_disconnected > DISCONNECT_SECONDS:
                if SHUTDOWN_ON_DISCONNECT:
                    self.force_disconnect('Realtick Gateway connection timed out after %d seconds' % self.seconds_disconnected)
        self.CheckPendingResults()

        if not int(time.time()) % 60:
            self.EveryMinute()

    def EveryMinute(self):
        if self.callback_metrics:
            self.output('callback_metrics: %s' % json.dumps(self.callback_metrics))   

    def WriteAllClients(self, msg):
        if self.log_client_messages:
            self.output('WriteAllClients: %s.%s' % (self.channel, msg))
        msg = str('%s.%s' % (self.channel, msg))
        for c in self.clients:
            c.sendString(msg)

    def error_handler(self, id, msg):
        """report error messages"""
        self.output('ALERT: %s %s' % (id, msg))
        self.WriteAllClients('error: %s %s' % (id, msg))

    def force_disconnect(self, reason):
        self.update_connection_status('Disconnected')
        self.error_handler(self.id, 'API Disconnect: %s' % reason)
        reactor.stop()

    def parse_tql_float(self, data, pid, label):
        ret = self.parse_tql_field(data, pid, label)
        return round(float(ret),2) if ret else 0.0

    def parse_tql_int(self, data, pid, label):
        ret = self.parse_tql_field(data, pid, label)
        return int(ret) if ret else 0

    def parse_tql_str(self, data, pid, label):
        ret = self.parse_tql_field(data, pid, label)
        return str(ret) if ret else ''
 
    def parse_tql_time(self, data, pid, label):
        """Parse TQL ascii time field returning datetime.time"""
        field = self.parse_tql_field(data, pid, label)
        if field:
            hour, minute, second = [int(i) for i in field.split(':')[0:3]]
            field = datetime.time(hour, minute, second)
        return field

    def parse_tql_date(self, data, pid, label):
        field = self.parse_tql_field(data, pid, label)
        if field:
                year, month, day = [int(i) for i in field.split('-')[0:3]]
                field = datetime.date(year, month, day)
        return field

    def parse_tql_field(self, data, pid, label):
        if str(data).lower().startswith('error '):
            if data.lower()=='error 0':
                code = 'Field Not Found'
            elif data.lower() == 'error 2':
                code = 'Field No Value'
            elif data.lower() == 'error 3':
                code = 'Field Not Permissioned'
            elif data.lower() == 'error 17':
                code = 'No Record Exists'
            elif data.lower() == 'error 256':
                code = 'Field Reset'
            else:
                code = 'Unknown Field Error'
            self.error_handler(pid, 'Field Parse Failure: %s=%s (%s)' % (label, repr(data), code))
            ret = None
        else:
            ret = data
        return ret

    def handle_time(self, rows):
        rows = json.loads(rows)
        if rows:
            time_field = rows[0]['TRDTIM_1']
            date_field = rows[0]['TRD_DATE']
            if time_field == 'Error 17':
                # this indicates the $TIME symbol is not found on the server, which is a kludge to determine the login has failed
                self.force_disconnect('Gateway reports $TIME symbol unknown; connection has failed')
            elif str(time_field).lower().startswith('error'):
                self.error_handler(self.id, 'handle_time: time field %s' % time_field)
            else:
                year, month, day = [int(i) for i in date_field.split('-')[0:3]]
                hour, minute, second = [int(i) for i in time_field.split(':')[0:3]]
                self.feed_now = datetime.datetime(year,month,day,hour,minute,second) + datetime.timedelta(seconds=self.time_offset)
                self.now = self.localize_time(self.feed_now)
		# don't add time offset
                if minute != self.last_minute:
                    self.last_minute = minute
                    self.WriteAllClients('time: %s %s:00' % (self.now.strftime('%Y-%m-%d'), self.now.strftime('%H:%M')))
        else:
            self.error_handler(self.id, 'handle_time: unexpected null input')

    def localize_time(self, apitime):
        """return API time corrected for local timezone"""
        return self.feedzone.localize(apitime).astimezone(self.localzone)
  
    def unlocalize_time(self, apitime):
        """reverse localize_time to convert local timezone to API time"""
        return self.localzone.localize(apitime).astimezone(self.feedzone)

    def handle_time_error(self, error):
        #time timeout error is reported as an expired callback
        self.output('time_error: %s' % repr(error))

    def connect(self):
        self.update_connection_status('Connecting')
        self.output('Awaiting startup response from RTX gateway at %s:%d...' % (self.api_hostname, self.api_port))

    def market_order(self, account, route, symbol, quantity, callback):
        return self.submit_order(account, route, 'market', 0, 0, symbol, int(quantity), callback)

    def limit_order(self, account, route, symbol, limit_price, quantity, callback):
        return self.submit_order(account, route, 'limit', float(limit_price), 0, symbol, int(quantity), callback)

    def stop_order(self, account, route, symbol, stop_price, quantity, callback):
        return self.submit_order(account, route, 'stop', 0, float(stop_price), symbol, int(quantity), callback)

    def stoplimit_order(self, account, route, symbol, stop_price, limit_price, quantity, callback):
        return self.submit_order(account, route, 'stoplimit', float(limit_price), float(stop_price), symbol, int(quantity), callback)

    def stage_market_order(self, tag, account, route, symbol, quantity, callback):
        return self.submit_order(account, route, 'market', 0, 0, symbol, int(quantity), callback, staged=tag)

    def create_order_id(self):
        return str(uuid1())

    def create_staged_order_ticket(self, account, callback):

        if not self.verify_account(account):
          API_Callback(self, 0, 'create-staged-order-ticket', callback).complete({'status': 'Error', 'errorMsg': 'account unknown'})
          return

        o=OrderedDict({})
        self.verify_account(account)
        bank, branch, customer, deposit = account.split('.')[:4]
        o['BANK']=bank
        o['BRANCH']=branch
        o['CUSTOMER']=customer
        o['DEPOSIT']=deposit
        tid = 'T-%s' % self.create_order_id() 
        o['CLIENT_ORDER_ID']=tid
        o['DISP_NAME']='N/A'
        o['STYP']=RTX_STYPE # stock
        o['EXIT_VEHICLE']='NONE'
        o['TYPE']='UserSubmitStagedOrder'

        # create callback to return to client after initial order update
        cb = API_Callback(self, tid, 'ticket', callback, self.callback_timeout['ORDER'])
        self.ticket_callbacks.append(cb)
        self.pending_tickets[tid]=API_Order(self, tid, o, 'client', cb)
        fields= ','.join(['%s=%s' %(i,v) for i,v in o.iteritems()])

        acb = API_Callback(self, tid, 'ticket-ack', RTX_LocalCallback(self, self.ticket_submit_ack_callback), self.callback_timeout['ORDER'])
        cb = API_Callback(self, tid, 'ticket', RTX_LocalCallback(self, self.ticket_submit_callback), self.callback_timeout['ORDER'])
        self.cxn_get('ACCOUNT_GATEWAY', 'ORDER').poke('ORDERS', '*', '', fields, acb, cb)
        # TODO: add cb and acb to callback lists so they can be tested for timeout

    def ticket_submit_ack_callback(self, data):
        """called when staged order ticket request has been submitted with 'poke' and Ack has returned""" 
        self.output('staged order ticket submission acknowledged: %s' % repr(data))

    def ticket_submit_callback(self, data):
        """called when staged order ticket request has been submitted with 'poke' and OnOtherAck has returned""" 
        self.output('staged order ticket submitted: %s' % repr(data))

    def submit_order(self, account, route, order_type, price, stop_price, symbol, quantity, callback, staged=None, oid=None):

        if not self.verify_account(account):
          API_Callback(self, 0, 'submit-order', callback).complete({'status': 'Error', 'errorMsg': 'account unknown'})
          return
        #bank, branch, customer, deposit = self.current_account.split('.')[:4]
        self.set_order_route(route, None)
        if type(self.order_route) != dict:
          API_Callback(self, 0, 'submit-order', callback).complete({'status': 'Error', 'errorMsg': 'undefined order route: %s' % repr(self.order_route)})
          return

        o=OrderedDict({})
        bank, branch, customer, deposit = account.split('.')[:4]
        o['BANK']=bank
        o['BRANCH']=branch
        o['CUSTOMER']=customer
        o['DEPOSIT']=deposit

        o['BUYORSELL']='Buy' if quantity > 0 else 'Sell' # Buy Sell SellShort
        o['quantity'] = quantity
        o['GOOD_UNTIL']='DAY' # DAY or YYMMDDHHMMSS
        route = self.order_route.keys()[0]
        o['EXIT_VEHICLE']=route
        
        # if order_route has a value, it is a dict of order route parameters
        if self.order_route[route]:
            for k,v in self.order_route[route].items():
                # encode strategy parameters in 0x01 delimited format
                if k in ['STRAT_PARAMETERS', 'STRAT_REDUNDANT_DATA']:
                    v = ''.join(['%s\x1F%s\x01' % i for i in v.items()])
                o[k]=v

        o['DISP_NAME']=symbol
        o['STYP']=RTX_STYPE # stock

        if symbol in self.primary_exchange_map.keys():
            exchange = self.primary_exchange_map[symbol]
        else:
            exchange = RTX_EXCHANGE
        o['EXCHANGE']=exchange
        
        if order_type == 'market':
            o['PRICE_TYPE'] = 'Market'
        elif order_type=='limit':
            o['PRICE_TYPE']='AsEntered' 
            o['PRICE']=price
        elif order_type=='stop':
            o['PRICE_TYPE']='Stop' 
            o['STOP_PRICE']=stop_price
        elif type=='stoplimit':
            o['PRICE_TYPE']='StopLimit' 
            o['STOP_PRICE']=stop_price
            o['PRICE']=price
        else:
            msg = 'unknown order type: %s' % order_type
            self.error_handler(self.id, msg)
            raise Exception(msg)

        o['VOLUME_TYPE']='AsEntered'
        o['VOLUME']=abs(quantity)
        
        if staged:
            o['ORDER_TAG'] = staged
            staging = 'Staged'
        else:
            staging = ''

        if oid:
            o['REFERS_TO_ID'] = oid
            submission = 'Change'
        else:
            oid = self.create_order_id()
            o['CLIENT_ORDER_ID']=oid
            submission = 'Order'
            
        o['TYPE'] = 'UserSubmit%s%s' % (staging, submission)

        # create callback to return to client after initial order update
        cb = API_Callback(self, oid, 'order', callback, self.callback_timeout['ORDER'])
        self.order_callbacks.append(cb)
        if oid in self.orders:
            self.pending_orders[oid]=self.orders[oid]
            self.orders[oid].callback = cb
        else:
            self.pending_orders[oid]=API_Order(self, oid, o, 'client', cb)

        fields= ','.join(['%s=%s' %(i,v) for i,v in o.iteritems() if i[0].isupper()])

        acb = API_Callback(self, oid, 'order-ack', RTX_LocalCallback(self, self.order_submit_ack_callback), self.callback_timeout['ORDER'])
        cb = API_Callback(self, oid, 'order', RTX_LocalCallback(self, self.order_submit_callback), self.callback_timeout['ORDER'])
        self.cxn_get('ACCOUNT_GATEWAY', 'ORDER').poke('ORDERS', '*', '', fields, acb, cb)

    def order_submit_ack_callback(self, data):
        """called when order has been submitted with 'poke' and Ack has returned""" 
        self.output('order submission acknowleded: %s' % repr(data))

    def order_submit_callback(self, data):
        """called when order has been submitted with 'poke' and OnOtherAck has returned""" 
        self.output('order submitted: %s' % repr(data))

    def cancel_order(self, oid, callback):
        self.output('cancel_order %s' % oid)
        cb = API_Callback(self, oid, 'cancel_order', callback, self.callback_timeout['ORDER'])
        order = self.orders[oid] if oid in self.orders else None
        if order:
            if order.fields['status'] == 'Canceled':
                cb.complete({'status': 'Error', 'errorMsg': 'Already canceled.', 'id': oid})
            else:
                msg=OrderedDict({})
                #for fid in ['DISP_NAME', 'STYP', 'ORDER_TAG', 'EXIT_VEHICLE']:
                #    if fid in order.fields:
                #        msg[fid] = order.fields[fid]
                msg['TYPE']='UserSubmitCancel'
                msg['REFERS_TO_ID']=oid
                fields= ','.join(['%s=%s' %(i,v) for i,v in msg.iteritems()])
                self.cxn_get('ACCOUNT_GATEWAY', 'ORDER').poke('ORDERS', '*', '', fields, None, cb)
                self.cancel_callbacks.append(cb)
        else:
            cb.complete({'status': 'Error', 'errorMsg': 'Order not found', 'id': oid})

    def symbol_enable(self, symbol, client, callback):
        self.output('symbol_enable(%s,%s,%s)' % (symbol, client, callback))
        if not symbol in self.symbols.keys():
            cb = API_Callback(self, symbol, 'add-symbol', callback, self.callback_timeout['ADDSYMBOL'])
            API_Symbol(self, symbol, client, cb)
            self.add_symbol_callbacks.append(cb)
        else:
            self.symbols[symbol].add_client(client)
            API_Callback(self, symbol, 'add-symbol', callback).complete(True)
        self.output('symbol_enable: symbols=%s' % repr(self.symbols))

    def symbol_init(self, symbol):
        ret = not 'SYMBOL_ERROR' in symbol.rawdata.keys()
        if not ret:
            self.symbol_disable(symbol.symbol, list(symbol.clients)[0])
        symbol.callback.complete(ret)
        return ret

    def symbol_disable(self, symbol, client):
        self.output('symbol_disable(%s,%s)' % (symbol, client))
        self.output('self.symbols=%s' % repr(self.symbols))
        if symbol in self.symbols.keys():
            ts = self.symbols[symbol]
            ts.del_client(client)
            if not ts.clients:
                del(self.symbols[symbol])
            self.output('ret True: self.symbols=%s' % repr(self.symbols))
            return True
        self.output('ret False: self.symbols=%s' % repr(self.symbols))

    def update_connection_status(self, status):
        self.connection_status = status
        if status != self.last_connection_status:
            self.last_connection_status = status
            self.WriteAllClients('connection-status-changed: %s' % status)

    def request_accounts(self, callback):
        cb = API_Callback(self, 0, 'request-accounts', callback, self.callback_timeout['ACCOUNT'])
        if self.accounts:
            cb.complete(self.accounts)
        elif self.account_request_pending:
            self.account_request_callbacks.append(cb)
        else:
            self.output(
                'Error: request_accounts; no data, but no account_request_pending')
            cb.complete(None)

    def request_positions(self, callback):
        cxn = self.cxn_get('ACCOUNT_GATEWAY', 'ORDER')
        cb = API_Callback(self, 0, 'positions', callback, self.callback_timeout['POSITION'])
        cxn.request('POSITION', '*', '', cb)
        self.position_callbacks.append(cb)

    def request_tickets(self, callback):
        self._request_orders(callback, 'tickets')

    def request_orders(self, callback):
        self._request_orders(callback, 'orders')

    def _request_orders(self, callback, label):
        cxn = self.cxn_get('ACCOUNT_GATEWAY', 'ORDER')
        cb = API_Callback(self, 0, label, callback, self.callback_timeout['ORDERSTATUS'])
        cxn.request('ORDERS', '*', '', cb)
        self.openorder_callbacks.append(cb)

    def request_order(self, oid, callback):
        cb = API_Callback(self, oid, 'order_status', callback, self.callback_timeout['ORDERSTATUS'])
        self.cxn_get('ACCOUNT_GATEWAY', 'ORDER').request('ORDERS', '*', "ORIGINAL_ORDER_ID='%s'" % oid, cb)
        self.order_status_callbacks.append(cb)

    def request_executions(self, callback):
        cb = API_Callback(self, 0, 'executions', callback, self.callback_timeout['ORDERSTATUS'])
        self.cxn_get('ACCOUNT_GATEWAY', 'ORDER').request('ORDERS', '*', '', cb)
        self.execution_callbacks.append(cb)

    def request_account_data(self, account, fields, callback):
        cxn = self.cxn_get('ACCOUNT_GATEWAY', 'ORDER')
        cb = API_Callback(self, 0, 'account_data', callback, self.callback_timeout['ACCOUNT'])
        bank, branch, customer, deposit = account.split('.')[:4]
        tql_where = "BANK='%s',BRANCH='%s',CUSTOMER='%s',DEPOSIT='%s'" % (bank,branch,customer,deposit)
        if fields:
            fields = ','.join(fields)
        else:
            fields = '*'
        cxn.request('DEPOSIT', fields, tql_where, cb)
        self.accountdata_callbacks.append(cb)

    def request_global_cancel(self):
        self.rtx_request('ACCOUNT_GATEWAY', 'ORDER', 
                        'ORDERS', 'ORDER_ID,ORIGINAL_ORDER_ID,CURRENT_STATUS,TYPE', "CURRENT_STATUS={'LIVE','PENDING'}",
                        'global_cancel', self.handle_global_cancel, self.openorder_callbacks, self.callback_timeout['ORDER'])

    def handle_global_cancel(self, rows):
        rows = json.loads(rows)
        for row in rows:
            if row['CURRENT_STATUS'] in ['LIVE', 'PENDING']:
                self.cancel_order(row['ORIGINAL_ORDER_ID'], RTX_LocalCallback(self, self.global_cancel_callback))

    def global_cancel_callback(self, data):
        data = json.loads(data)
        self.output('global cancel: %s' % repr(data))

    def _fail_query_bars(self, msg, callback):
        self.error_handler(self.id, msg)
        API_Callback(self, 0, 'query-bars-failed', callback).complete(None)
        return None

    def query_bars(self, symbol, interval, bar_start, bar_end, callback):

        if not self.enable_barchart:
            return self._fail_query_bars('ALERT: query_bars unimplemented', callback)

        if not symbol in self.symbols:
            return self._fail_query_bars('query_bars failed: symbol %s not active' % symbol, callback)

        # intraday n-minute bars; given stop date, number of days, minutes_per_bar
        if str(interval).startswith('D'):
            table = 'DAILY'
            interval = 0
        elif str(interval).startswith('W'):
            table = 'DAILY'
            interval = 1
        elif str(interval).startswith('M'):
            table = 'DAILY'
            interval = 2
        else:
            table = 'INTRADAY'
            interval = int(interval)

        session_start = datetime.datetime.strptime(self.symbols[symbol].rawdata['STARTTIME'], '%H:%M:%S')
        session_stop = datetime.datetime.strptime(self.symbols[symbol].rawdata['STOPTIME'], '%H:%M:%S')
        #print('barchart session_start=%s session_stop=%s' % (session_start, session_stop))
 
        # if start time is a negative integer, use it as an offset from the end time
        # limit start and end to the session start and stop times
        if str(bar_start).startswith('-'):
            offset = int(str(bar_start))
            bar_end = self.feed_now + datetime.timedelta(minutes=1)
            if bar_end.time() > session_stop.time():
                bar_end = datetime.datetime(bar_end.year, bar_end.month, bar_end.day, session_stop.hour, session_stop.minute, 0)
            if table=='DAILY':
                delta = [datetime.timedelta(days=offset), datetime.timedelta(weeks=offset), datetime.timedelta(days=offset*30)][interval]
            else:
                delta = datetime.timedelta(minutes=offset * interval)
            bar_start = bar_end + delta
            if bar_start.time() < session_start.time():
                bar_start = datetime.datetime(bar_start.year, bar_start.month, bar_start.day, session_start.hour, session_start.minute, 0)
            #print('offset bar start: start=%s end=%s' % (repr(bar_start), repr(bar_end)))
        else:
            # implement defaults for bar_start, bar_end
            if bar_start=='.':
                bar_start = self.feed_now.date().isoformat()
            elif re.match('^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d$', bar_start):
                # bar_start provided with time; adjust timezone
                bar_start = self.unlocalize_time(datetime.datetime.strptime(bar_start, '%Y-%m-%d %H:%M:%S')).isoformat(' ')[:19]
            elif not re.match('^\d\d\d\d-\d\d-\d\d$', bar_start):
                return self._fail_query_bars('query_bars: bad parameter format bar_start=%s' % bar_start, callback) 
                
            if bar_end=='.':
                bar_end = bar_start[:10]
            elif re.match('^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d$', bar_end):
                bar_end = self.unlocalize_time(datetime.datetime.strptime(bar_end, '%Y-%m-%d %H:%M:%S')).isoformat(' ')[:19]
            elif not re.match('^\d\d\d\d-\d\d-\d\d$', bar_end):
                return self._fail_query_bars('query_bars: bad parameter format bar_end=%s' % bar_end, callback) 

            if len(bar_start) == 10:
                bar_start += session_start.time().strftime(' %H:%M:%S')

            if len(bar_end) == 10:
                bar_end += session_stop.time().strftime(' %H:%M:%S')

            #print('+++ bar_start=%s bar_end=%s' % (repr(bar_start), repr(bar_end)))
            bar_start = datetime.datetime.strptime(bar_start, '%Y-%m-%d %H:%M:%S')
            bar_end = datetime.datetime.strptime(bar_end, '%Y-%m-%d %H:%M:%S')
   
        # limit bar_start and bar_end to stay within session start, stop
        if bar_start.time() < session_start.time() or table=='DAILY':
            bar_start = datetime.datetime(bar_start.year, bar_start.month, bar_start.day, session_start.hour, session_start.minute, 0)

        if bar_end.time() > session_stop.time() or table=='DAILY':
            bar_end = datetime.datetime(bar_end.year, bar_end.month, bar_end.day, session_stop.hour, session_stop.minute, 0)

        where = ','.join([
	    "DISP_NAME='%s'" % symbol,
            "BARINTERVAL=%d" % interval,
            "STARTDATE='%s'" % bar_start.strftime('%Y/%m/%d'),
            "CHART_STARTTIME='%s'" % bar_start.strftime('%H:%M'),
            "STOPDATE='%s'" % bar_end.strftime('%Y/%m/%d'),
            "CHART_STOPTIME='%s'" % bar_end.strftime('%H:%M'),
        ])

        #print('barchart where=%s' % repr(where))

        cb = API_Callback(self, '%s;%s' % (table, where), 'barchart', callback, self.callback_timeout['BARCHART'])
        self.cxn_get('TA_SRV', BARCHART_TOPIC).request(table, BARCHART_FIELDS, where, cb)
        self.bardata_callbacks.append(cb)

    def format_barchart(self, rows):
        #pprint({'format_barchart': rows})
        bars = None 
        if type(rows) == list and len(rows)==1:
            row = rows[0]
            # DAILY bars have no time values, so spoof for the parser
            if row['TRDTIM_1']=='Error 17':
                symbol = self.symbols[row['DISP_NAME']]
                session_start = symbol.rawdata['STARTTIME']
                row['TRDTIM_1'] = [session_start for t in row['TRD_DATE']]
            types = {k:type(v) for k, v in row.iteritems()}
            #print('types = %s' % repr(types))
            if types=={
                'DISP_NAME': unicode,
                'TRD_DATE': list,
                'TRDTIM_1': list,
                'OPEN_PRC': list,
                'HIGH_1': list,
                'LOW_1': list,
                'SETTLE': list,
                'ACVOL_1': list
            }:
                bars = [ 
                    self.format_barchart_date(row['TRD_DATE'][i], row['TRDTIM_1'][i], self.id)
                    + [
                            self.parse_tql_float(row['OPEN_PRC'][i], self.id, 'OPEN_PRC'),
                            self.parse_tql_float(row['HIGH_1'][i], self.id, 'HIGH_1'),
                            self.parse_tql_float(row['LOW_1'][i], self.id, 'LOW_1'),
                            self.parse_tql_float(row['SETTLE'][i], self.id, 'SETTLE'),
                            self.parse_tql_int(row['ACVOL_1'][i], self.id, 'ACVOL_1')
                    ] for i in range(len(row['TRD_DATE']))
                ]
        if not bars:
            self.error_handler(self, 'barchart data format failed: %s' % repr(rows))
        return bars

    def format_barchart_date(self, bdate, btime, pid):
        """return date and time as tuple ('yyyy-mm-dd', 'hh:mm:ss') or ('', '')""" 
        bar_date = self.parse_tql_date(bdate, pid, 'TRD_DATE')
        bar_time = self.parse_tql_time(btime, pid, 'TRDTIM_1')
        if bar_date and bar_time:
            bartime = datetime.datetime.combine(bar_date, bar_time)
            bartime = self.localize_time(bartime)
            ret = bartime.isoformat()[:19].split('T')
        else:
            ret = ['', '']
        return ret

    def query_connection_status(self):
        return self.connection_status

    def set_order_route(self, route, callback):
        #print('set_order_route(%s, %s) type=%s %s' % (repr(route), repr(callback), type(route), (type(route) in [str, unicode])))
        if type(route) in [str, unicode]:
            if route.startswith('{'):
                route = json.loads(route)
	    elif route.startswith('"'):
                route = {json.loads(route): None}
            else:
                route = {route: None}
        if (type(route)==dict) and (len(route.keys()) == 1) and (type(route.keys()[0]) in [str, unicode]):
            self.order_route = route
            if callback:
                self.get_order_route(callback)
        else:
            if callback:
                callback.errback(Failure(Exception('cannot set order route %s' % route)))
            else:
                self.error_handler(None, 'Cannot set order route %s' % repr(route))

    def get_order_route(self, callback):
        API_Callback(self, 0, 'get_order_route', callback).complete(self.order_route)