def get_quote(): client_config = get_client_config() quote_client = QuoteClient(client_config) quote_client.get_market_status(Market.US) quote_client.get_briefs(symbols=['AAPL', '00700', '600519'], include_ask_bid=True, right=QuoteRight.BR) #quote_client.get_timeline('AAPL', period=TimelinePeriod.DAY, include_hour_trading=True) return quote_client.get_bars('AAPL')
class Tiger: # default logger defualt_logger = logging.getLogger('bc_tiger_logger') # init def __init__(self, account_type, config, sandbox_debug=False, logger_name=None, open_time_adj=0, close_time_adj=0): # get logger self.logger = Tiger.defualt_logger if ( logger_name is None) else logging.getLogger(logger_name) # read user info, position record from local files self.__user_info = io_util.read_config(file_path=config['tiger_path'], file_name='user_info.json') self.__position_record = io_util.read_config( file_path=config['config_path'], file_name='tiger_position_record.json') self.record = self.__position_record[account_type].copy() self.eod_api_key = config['api_key']['eod'] # set account, account type self.account = self.__user_info[account_type] self.account_type = account_type # initialize client_config self.client_config = TigerOpenClientConfig(sandbox_debug=sandbox_debug) self.client_config.private_key = read_private_key( config['tiger_path'] + self.__user_info['private_key_name']) self.client_config.tiger_id = str(self.__user_info['tiger_id']) self.client_config.language = Language.en_US self.client_config.account = self.account # get quote/trade clients, assets, positions self.quote_client = QuoteClient(self.client_config) self.trade_client = TradeClient(self.client_config) self.positions = self.trade_client.get_positions(account=self.account) self.assets = self.trade_client.get_assets(account=self.account) # get market status and trade time self.update_trade_time(open_time_adj=open_time_adj, close_time_adj=close_time_adj) # update position record self.synchronize_position_record(config=config) self.logger.info(f'[tiger]: Tiger instance created: {logger_name}') # get user info def get_user_info(self): return self.__user_info # get position record def get_position_record(self): return self.__position_record # synchronize position record with real position status def synchronize_position_record(self, config): account_type = self.account_type # initialize position record for symbols that not in position record init_cash = config['trade']['init_cash'][account_type] pool = config['selected_sec_list'][config['trade']['pool'] [account_type]] for symbol in pool: if symbol not in self.record.keys(): self.record[symbol] = {'cash': init_cash, 'position': 0} # get real position (dict) position_dict = dict([(x.contract.symbol, x.quantity) for x in self.positions]) # compare position record with real position record_conflicted = False for symbol in self.record.keys(): if symbol not in pool: continue # update position in record record_position = self.record[symbol]['position'] current_position = 0 if ( symbol not in position_dict.keys()) else position_dict[symbol] if current_position != record_position: record_conflicted = True if current_position > 0: self.record[symbol] = { 'cash': 0, 'position': current_position } else: self.record[symbol] = {'cash': init_cash, 'position': 0} self.logger.error( f'[{account_type[:4]}]: {symbol} position({current_position}) rather than ({record_position}), reset record' ) # add record for position that not recorded for symbol in [ x for x in position_dict.keys() if (x in pool and x not in self.record.keys()) ]: record_conflicted = True self.record[symbol] = { 'cash': 0, 'position': position_dict[symbol] } self.logger.error( f'[{account_type[:4]}]: {symbol} position({position_dict[symbol]}) not in record, add record' ) # update __position_record if record_conflicted: self.__position_record[self.account_type] = self.record.copy() io_util.create_config_file(config_dict=self.__position_record, file_path=config['config_path'], file_name='tiger_position_record.json') # update position for an account def update_position_record(self, config, init_cash=None, init_position=None, start_time=None, end_time=None, is_print=True): # set default values init_cash = config['trade']['init_cash'][self.account_type] if ( init_cash is None) else init_cash init_position = 0 if (init_position is None) else init_position start_time = self.trade_time['pre_open_time'].strftime( format="%Y-%m-%d %H:%M:%S") if (start_time is None) else start_time end_time = self.trade_time['post_close_time'].strftime( format="%Y-%m-%d %H:%M:%S") if (end_time is None) else end_time try: # get today filled orders orders = self.trade_client.get_filled_orders(start_time=start_time, end_time=end_time) # update position records for order in orders: symbol = order.contract.symbol action = order.action quantity = order.quantity - order.remaining commission = order.commission avg_fill_price = order.avg_fill_price # init record if not exist if symbol not in self.record.keys(): self.record[symbol] = { 'cash': init_cash, 'position': init_position } record_cash = self.record[symbol]['cash'] record_position = self.record[symbol]['position'] # calculate new cash and position if action == 'BUY': cost = avg_fill_price * quantity + commission new_cash = record_cash - cost new_position = record_position + quantity elif action == 'SELL': acquire = avg_fill_price * quantity - commission new_cash = record_cash + acquire new_position = record_position - quantity else: new_cash = record_cash new_position = record_position # update record if new_cash >= 0 and new_position >= 0: self.record[symbol]['cash'] = new_cash self.record[symbol]['position'] = new_position if is_print: self.logger.info( f'[{self.account_type[:4]}]: updating position record for {symbol} {record_cash, record_position} -> {new_cash, new_position}' ) # update __position_record # self.record['updated'] = datetime.datetime.now().strftime(format="%Y-%m-%d %H:%M:%S") self.__position_record = io_util.read_config( file_path=config['config_path'], file_name='tiger_position_record.json') self.__position_record[self.account_type] = self.record.copy() self.__position_record['updated'][ self.account_type] = datetime.datetime.now().strftime( format="%Y-%m-%d %H:%M:%S") io_util.create_config_file(config_dict=self.__position_record, file_path=config['config_path'], file_name='tiger_position_record.json') except Exception as e: self.logger.exception( f'[erro]: fail updating position records for {self.account_type}, {e}' ) # update portfolio for an account def update_portfolio_record(self, config, position_summary=None, is_print=True): # get position summary if position_summary is None: position_summary = self.get_position_summary(get_briefs=False) position_summary.set_index('symbol', inplace=True) position_summary = position_summary.round(2) # get asset summary net_value = 0 market_value = 0 cash = 0 asset_summary = self.get_asset_summary() if len(asset_summary) > 0: net_value = asset_summary.loc[0, 'net_value'] market_value = asset_summary.loc[0, 'holding_value'] cash = asset_summary.loc[0, 'cash'] # post process if market_value == float('inf'): market_value = position_summary['market_value'].sum().round(2) # load portfolio record portfolio_record = io_util.read_config(file_path=config['config_path'], file_name='portfolio.json') old_net_value = portfolio_record['tiger'][self.account_type].get( 'net_value') support = portfolio_record['tiger'][self.account_type].get( 'portfolio').get('support') resistant = portfolio_record['tiger'][self.account_type].get( 'portfolio').get('resistant') # update portfolio record for current account portfolio_record['tiger'][ self.account_type]['portfolio'] = position_summary.to_dict() portfolio_record['tiger'][ self.account_type]['portfolio']['support'] = {} portfolio_record['tiger'][ self.account_type]['portfolio']['resistant'] = {} quantity = portfolio_record['tiger'][ self.account_type]['portfolio'].get('quantity') if quantity is not None: if support is not None: for symbol in quantity.keys(): portfolio_record['tiger'][self.account_type]['portfolio'][ 'support'][symbol] = support.get(symbol) if resistant is not None: for symbol in quantity.keys(): portfolio_record['tiger'][self.account_type]['portfolio'][ 'resistant'][symbol] = resistant.get(symbol) portfolio_record['tiger'][ self.account_type]['market_value'] = market_value portfolio_record['tiger'][self.account_type]['net_value'] = net_value portfolio_record['tiger'][self.account_type]['cash'] = cash portfolio_record['tiger'][ self.account_type]['updated'] = datetime.datetime.now().strftime( format="%Y-%m-%d %H:%M:%S") io_util.create_config_file(config_dict=portfolio_record, file_path=config['config_path'], file_name='portfolio.json') # print if is_print: self.logger.info( f'[{self.account_type[:4]}]: net value {old_net_value} --> {net_value}' ) # get summary of positions def get_position_summary(self, get_briefs=False): try: # update positions self.positions = self.trade_client.get_positions( account=self.client_config.account) # convert positions(list) to dataframe if len(self.positions) > 0: result = { 'symbol': [], 'quantity': [], 'average_cost': [], 'market_price': [] } for pos in self.positions: result['symbol'].append(pos.contract.symbol) result['quantity'].append(pos.quantity) result['average_cost'].append(pos.average_cost) result['market_price'].append(pos.market_price) result = pd.DataFrame(result) # get briefs for stocks in positions if get_briefs: status = io_util.get_stock_briefs( symbols=[x.contract.symbol for x in self.positions], source='eod', period='1d', interval='1m', api_key=self.eod_api_key) result = pd.merge(result, status, how='left', left_on='symbol', right_on='symbol') result['rate'] = round( (result['latest_price'] - result['average_cost']) / result['average_cost'], 2) result = result[[ 'symbol', 'quantity', 'average_cost', 'latest_price', 'rate', 'latest_time' ]] else: result.rename(columns={'market_price': 'latest_price'}, inplace=True) result['rate'] = round( (result['latest_price'] - result['average_cost']) / result['average_cost'], 2) result['latest_time'] = None # calculate market value result['market_value'] = result['quantity'] * result[ 'latest_price'] else: result = pd.DataFrame({ 'symbol': [], 'quantity': [], 'average_cost': [], 'latest_price': [], 'rate': [], 'market_value': [], 'latest_time': [] }) except Exception as e: result = pd.DataFrame({ 'symbol': [], 'quantity': [], 'average_cost': [], 'latest_price': [], 'rate': [], 'market_value': [], 'latest_time': [] }) self.logger.exception(f'[erro]: can not get position summary: {e}') return result # get summary of assets def get_asset_summary(self, print_summary=False): # update assets self.assets = self.trade_client.get_assets( account=self.client_config.account) asset = self.assets[0] result = { 'account': [asset.account], 'net_value': [asset.summary.net_liquidation], 'holding_value': [asset.summary.gross_position_value], 'cash': [asset.summary.cash], 'available_casg': [asset.summary.available_funds], 'pnl': [asset.summary.realized_pnl], 'holding_pnl': [asset.summary.unrealized_pnl] } if print_summary: summary = f''' 账户: {asset.account}({asset.summary.currency}): 总资产: {asset.summary.net_liquidation} 现金: {asset.summary.cash} (可用 {asset.summary.available_funds}) 持仓市值: {asset.summary.gross_position_value} 日内交易次数: {asset.summary.day_trades_remaining} 已实现盈亏: {asset.summary.realized_pnl} 未实现盈亏: {asset.summary.unrealized_pnl} ''' print(summary) return pd.DataFrame(result) # get available money def get_available_cash(self): # get available cash for real accounts self.assets = self.trade_client.get_assets( account=self.client_config.account) available_cash = self.assets[0].summary.cash return available_cash # get quantity of symbol currently in the position def get_in_position_quantity(self, symbol, get_briefs=False): # initialize affordable quantity quantity = 0 # get position summary position = self.get_position_summary(get_briefs=get_briefs) if len(position) > 0: position = position.set_index('symbol') if symbol in position.index: quantity = position.loc[symbol, 'quantity'] return quantity # check whether it is affordable to buy certain amount of a stock def get_affordable_quantity(self, symbol, cash=None, trading_fee=3): # initialize affordable quantity and available cash quantity = 0 available_cash = self.get_available_cash() if (cash is None) else cash # get latest price of stock stock_brief = io_util.get_stock_briefs( symbols=[symbol], source='eod', period='1d', interval='1m', api_key=self.eod_api_key).set_index('symbol') latest_price = stock_brief.loc[symbol, 'latest_price'] # check if it is affordable quantity = math.floor((available_cash - trading_fee) / latest_price) return quantity # idle for specified time and check position in certain frequency def idle(self, target_time, check_frequency=600): """ Sleep with a fixed frequency, until the target time :param target_time: the target time in datetime.datetime format :param check_frequency: the fixed sleep_time :returns: none :raises: none """ # get current time now = datetime.datetime.now() while now < target_time: # # get position summary # pos = self.get_position_summary() # self.logger.info(f'[rate]:----------------------------------------------\n{pos}\n') # get current time, calculate difference between current time and target time diff_time = round((target_time - now).total_seconds()) sleep_time = (diff_time + 1) if ( diff_time <= check_frequency) else check_frequency # sleep self.logger.info( f'[idle]: {now.strftime(format="%Y-%m-%d %H:%M:%S")}: sleep for {sleep_time} seconds' ) time.sleep(sleep_time) # update current time now = datetime.datetime.now() self.logger.info( f'[wake]: {now.strftime(format="%Y-%m-%d %H:%M:%S")}: exceed target time({target_time})' ) # update trade time def update_trade_time(self, market=Market.US, tz='Asia/Shanghai', open_time_adj=0, close_time_adj=0): # get local timezone tz = pytz.timezone(tz) try: # get open_time status = self.quote_client.get_market_status(market=market)[0] current_status = status.status open_time = status.open_time.astimezone(tz).replace(tzinfo=None) open_time = open_time + datetime.timedelta(hours=open_time_adj) # if program runs after market open, api will return trade time for next trade day, # trade time for current trade day need to be calculated manually if status.status in ['Trading', 'Post-Market Trading']: if open_time.weekday() == 0: open_time = open_time - datetime.timedelta(days=3) else: open_time = open_time - datetime.timedelta(days=1) # calculate close time, pre_open_time, post_close_time close_time = open_time + datetime.timedelta(hours=6.5 + close_time_adj) pre_open_time = open_time - datetime.timedelta(hours=5.5) post_close_time = close_time + datetime.timedelta(hours=4) # open and close time of chinese stock market a_open_time = pre_open_time + datetime.timedelta( hours=9.5 - pre_open_time.hour) a_close_time = pre_open_time + datetime.timedelta( hours=15 - pre_open_time.hour) except Exception as e: self.logger.error(e) current_status = None open_time = None close_time = None pre_open_time = None post_close_time = None self.trade_time = { 'status': current_status, 'tz': tz, 'pre_open_time': pre_open_time, 'open_time': open_time, 'close_time': close_time, 'post_close_time': post_close_time, 'a_open_time': a_open_time, 'a_close_time': a_close_time } # update market status def update_market_status(self, market=Market.US, return_str=False): try: # get market status status = self.quote_client.get_market_status(market=market)[0] self.trade_time['status'] = status.status if return_str: time_format = '%Y-%m-%d %H:%M' pre_open_time = self.trade_time['pre_open_time'].strftime( time_format) post_close_time = self.trade_time['post_close_time'].strftime( time_format) time_format = '%H:%M' open_time = self.trade_time['open_time'].strftime(time_format) close_time = self.trade_time['close_time'].strftime( time_format) time_str = f'<({pre_open_time}){open_time} -- {close_time}({post_close_time})>' return time_str except Exception as e: self.logger.error(e) # buy or sell stocks def trade(self, symbol, action, quantity, price=None, stop_loss=None, stop_profit=None, print_summary=True): trade_summary = '' try: # construct contract contract = stock_contract(symbol=symbol, currency='USD') # construct order if price is None: order_price = 'market' order = market_order(account=self.client_config.account, contract=contract, action=action, quantity=quantity) else: order_price = float(f'{price}') order = limit_order(account=self.client_config.account, contract=contract, action=action, quantity=quantity, limit_price=price) # construct trade summary trade_summary += f'[{action}]: {symbol} X {quantity} ({order_price})\t' # attach order legs order_legs = [] if stop_loss is not None: stop_loss_order_leg = order_leg('LOSS', stop_loss, time_in_force='GTC') # 附加止损单 order_legs.append(stop_loss_order_leg) if stop_profit is not None: stop_profit_order_leg = order_leg('PROFIT', stop_profit, time_in_force='GTC') # 附加止盈单 order_legs.append(stop_profit_order_leg) if len(order_legs) > 0: order.order_legs = order_legs # place buy order if affordable if action == 'BUY': affordable_quantity = self.get_affordable_quantity( symbol=symbol) if quantity <= affordable_quantity: self.trade_client.place_order(order) trade_summary += f'SUCCEED: {order.id}' else: trade_summary += f'FAILED: Not affordable({affordable_quantity}/{quantity})' # place sell order if holding enough stocks elif action == 'SELL': in_position_quantity = self.get_in_position_quantity(symbol) if in_position_quantity >= quantity: self.trade_client.place_order(order) trade_summary += f'SUCCEED: {order.id}' else: trade_summary += f'FAILED: Not enough stock to sell({in_position_quantity}/{quantity})' # other actions else: trade_summary += f'FAILED: Unknown action({action})' except Exception as e: trade_summary += f'FAILED: {e}' # print trade summary if print_summary: self.logger.info(trade_summary) return trade_summary # auto trade according to signals def signal_trade(self, signal, money_per_sec, order_type='market', trading_fee=5, pool=None, according_to_record=True, minimum_position=None): # set symbol to index if len(signal) > 0: # signal = signal.rename(columns={'代码':'symbol', '交易信号':'action'}) # signal = signal.set_index('symbol') # filter sec with pool if pool is not None: filtered_list = [x for x in signal.index if x in pool] signal = signal.loc[filtered_list, signal.columns].copy() # if signal list is not empty if len(signal) > 0: # get latest price for signals # if order_type == 'market': # signal_brief = self.quote_client.get_stock_briefs(symbols=signal.index.tolist()).set_index('symbol') # signal_brief = io_util.get_stock_briefs(symbols=signal.index.tolist(), source='eod', period='1d', interval='1m', api_key=self.eod_api_key).set_index('symbol') # signal = pd.merge(signal, signal_brief[['latest_price']], how='left', left_index=True, right_index=True) # get in-position quantity and latest price for signals position = self.get_position_summary(get_briefs=False) if len(position) == 0: position = pd.DataFrame({'symbol': [], 'quantity': []}) position = position.set_index('symbol') signal = pd.merge(signal, position[['quantity']], how='left', left_index=True, right_index=True).fillna(0) # sell # get sell signals sell_signal = signal.query('action == "s"') if len(sell_signal) > 0: # go through sell signals for symbol in sell_signal.index: # check whether symbol is in positions in_position_quantity = signal.loc[symbol, 'quantity'] if in_position_quantity > 0: if order_type == 'limit': price = signal.loc[symbol, 'latest_price'] else: price = None trade_summary = self.trade( symbol=symbol, action='SELL', quantity=in_position_quantity, price=price, print_summary=False) self.logger.info(trade_summary) else: self.logger.info( f'[SELL]: {symbol} skipped (not in positions)') else: self.logger.info(f'[SELL]: no signal') # buy # get available cash, set minimum position available_cash = self.get_available_cash() if minimum_position is None: minimum_position = money_per_sec # get buy signals which not in posiitons yet default_money_per_sec = money_per_sec buy_signal = signal.query('action == "b"') if len(buy_signal) > 0: # go through buy signals for symbol in buy_signal.index: # break when available cash is below 200 if available_cash <= minimum_position: self.logger.info( f'[BUY]: Available cash is too low({available_cash}/{minimum_position}), stop buying' ) break # check whether symbol is already in positions in_position_quantity = signal.loc[symbol, 'quantity'] if in_position_quantity == 0: # set money used to establish a new position if according_to_record: if (symbol in self.record.keys()) and ( self.record[symbol]['position'] == 0): money_per_sec = self.record[symbol]['cash'] else: money_per_sec = default_money_per_sec # check whether there is enough available money money_per_sec = available_cash if ( money_per_sec > available_cash) else money_per_sec # calculate quantity to buy quantity = math.floor( (money_per_sec - trading_fee) / signal.loc[symbol, 'latest_price']) if quantity > 0: if order_type == 'limit': price = signal.loc[symbol, 'latest_price'] else: price = None trade_summary = self.trade(symbol=symbol, action='BUY', quantity=quantity, price=price, print_summary=False) self.logger.info(trade_summary) # update available cash available_cash -= quantity * signal.loc[ symbol, 'latest_price'] else: self.logger.info(f'[BUY]: not enough money') continue else: self.logger.info( f'[BUY]: {symbol} skipped (already in positions:{in_position_quantity})' ) continue else: self.logger.info(f'[BUY]: no signal') else: self.logger.info(f'[SKIP]: no signal') # stop loss or stop profit or clear all positions def cash_out(self, stop_loss_rate=None, stop_profit_rate=None, clear_all=False, print_summary=True): # get current position with summary position = self.get_position_summary(get_briefs=True) if len(position) > 0: # set symbol as index position = position.set_index('symbol') # if clear all positions if clear_all: cash_out_list = position.index.tolist() else: stop_loss_list = [] if stop_loss_rate is None else position.query( f'rate < {stop_loss_rate}').index.tolist() stop_profit_list = [] if stop_profit_rate is None else position.query( f'rate > {stop_profit_rate}').index.tolist() cash_out_list = list(set(stop_loss_list + stop_profit_list)) # cash out if len(cash_out_list) > 0: cash_out_position = position.loc[cash_out_list, ].copy() self.logger.info( f'[STOP]: LOSS: {stop_loss_list}, PROFIT: {stop_profit_list}' ) for index, row in cash_out_position.iterrows(): self.trade(symbol=index, action='SELL', quantity=row['quantity'], print_summary=print_summary)