Path: blob/master/ invest-robot-contest_tinvest_robot-master/tinvest_robot_perevalov/trader.py
5933 views
import os1from uuid import uuid423from tinkoff.invest import Client4from tinkoff.invest import schemas56from tinvest_robot_perevalov import _db7from tinvest_robot_perevalov._config import _IS_SANDBOX, app_name, USD_FIGI, CLASS_CODE_SPB, FEE89import logging1011logging.basicConfig(12format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO13)1415logger = logging.getLogger(__name__)16171819def _quotation_to_float(q: schemas.Quotation) -> float:20"""Convert quotation to float2122Args:23q (schemas.Quotation): Quotation value to convert2425Returns:26float: converted float number27"""28return float(str(q.units) + '.' + str(q.nano))293031def _float_to_quotation(f: float) -> schemas.Quotation:32"""Convert float to quotation3334Args:35f (float): Float number to convert3637Returns:38schemas.Quotation: converted quotation number39"""40return schemas.Quotation(units=int(str(f).split('.')[0]), nano=int(str(f).split('.')[1][:10]))414243def _post_order(figi: str, quantity: int, price: schemas.Quotation, direction: schemas.OrderDirection, account_id: str, order_type: schemas.OrderType, news_id: int) -> schemas.PostOrderResponse:44"""Posts order to Tinkoff API4546Args:47figi (str): FIGI of share48quantity (int): quantity of share49price (schemas.Quotation): price of share50direction (schemas.OrderDirection): order direction (SELL or BUY)51account_id (str): account ID for order52order_type (schemas.OrderType): type of order (MARKET or LIMIT)53news_id (int): ID of news which caused order5455Returns:56schemas.PostOrderResponse: response from Tinkoff API or None57"""5859with Client(token=os.environ["TINVEST_TOKEN"], app_name=app_name) as client:60order_id = str(uuid4())61if _IS_SANDBOX:62response = client.sandbox.post_sandbox_order(figi=figi, quantity=quantity, direction=direction, account_id=account_id, order_type=order_type, order_id=order_id)63else:64response = client.orders.post_order(figi=figi, quantity=quantity, direction=direction, account_id=account_id, order_type=order_type, order_id=order_id)65_db.put_order_in_db(figi=figi, quantity=quantity, price=_quotation_to_float(response.initial_order_price_pt), direction=int(direction), account_id=account_id, order_type=int(order_type), order_id=order_id, news_id=news_id)66return response676869def _get_shares_by(tickers: list) -> list:70"""Get list of shares by tickers7172Args:73tickers (list): Tickers of shares7475Returns:76list: share objects77"""78shares = list()79with Client(token=os.environ["TINVEST_TOKEN"], app_name=app_name) as client:80for ticker in tickers:81try:82share = client.instruments.share_by(id_type=schemas.InstrumentIdType.INSTRUMENT_ID_TYPE_TICKER, class_code=CLASS_CODE_SPB, id=ticker).instrument83shares.append(share)84except Exception as e:85logger.error(str(e))86return shares878889def _get_position_by(positions: list, figi: str) -> schemas.PortfolioPosition:90"""Searches for position by its figi9192Args:93positions (list): list of positions (or any assets)94figi (str): figi of position to search for9596Returns:97schemas.PortfolioPosition: position object or None98"""99for p in positions:100if p.figi == figi:101return p102return None103104105def _get_best_price(share: object, direction: object) -> float:106"""Get best price for share from order book (for BUY orders take the first ask, for SELL orders take the first bid)107108Args:109share (object): share or asset to get price for110direction (object): direction of order (BUY or SELL)111112Returns:113float: best price for share or 0.0 if no orders in order book114"""115try:116with Client(token=os.environ["TINVEST_TOKEN"], app_name=app_name) as client:117order_book = client.market_data.get_order_book(figi=share.figi, depth=1)118if direction == schemas.OrderDirection.ORDER_DIRECTION_BUY:119return _quotation_to_float(order_book.asks[0].price)120elif direction == schemas.OrderDirection.ORDER_DIRECTION_SELL:121return _quotation_to_float(order_book.bids[0].price)122except Exception as e:123logger.error(str(e) + " Maybe order book is empty")124return 0.0125126def _get_balance(account_id: str, figi: str = USD_FIGI) -> float:127"""Get balance for account according to FIGI128129Args:130account_id (str): account id to get balance for131figi (str, optional): FIGI for getting balance. Defaults to USD_FIGI.132133Returns:134float: float number of balance135"""136with Client(token=os.environ["TINVEST_TOKEN"], app_name=app_name) as client:137if _IS_SANDBOX:138portfolio = client.sandbox.get_sandbox_portfolio(account_id=account_id)139position = _get_position_by(portfolio.positions, USD_FIGI)140return _quotation_to_float(position.quantity)141else:142portfolio = client.operations.get_portfolio(account_id=account_id)143position = _get_position_by(portfolio.positions, USD_FIGI)144return _quotation_to_float(position.quantity)145146147def _get_position_from_account(share: object, account_id: str) -> schemas.PortfolioPosition:148"""Get position from account149150Args:151share (object): a particular share to get position for152account_id (str): Account ID to get position for153154Returns:155float: found position or None156"""157with Client(token=os.environ["TINVEST_TOKEN"], app_name=app_name) as client:158if _IS_SANDBOX:159portfolio = client.sandbox.get_sandbox_portfolio(account_id=account_id)160position = _get_position_by(portfolio.positions, share.figi)161return position162else:163portfolio = client.operations.get_portfolio(account_id=account_id)164position = _get_position_by(portfolio.positions, share.figi)165return position166167168def _get_current_order(share: object, account_id: str) -> schemas.Order:169"""Get current order for share from account170171Args:172share (object): A particular share to get current order for173account_id (str): An account ID to get current order for174175Returns:176schemas.Order: an order object or None177"""178with Client(token=os.environ["TINVEST_TOKEN"], app_name=app_name) as client:179if _IS_SANDBOX:180orders = client.sandbox.get_sandbox_orders(account_id=account_id)181order = _get_position_by(orders.orders, share.figi)182return order183else:184orders = client.orders.get_orders(account_id=account_id)185order = _get_position_by(orders.orders, share.figi)186return order187188189def _handle_match(sentiment: str, share: object, account_id: str, news_id: int, quantity: int = 1) -> schemas.PostOrderResponse:190"""Handles occurence of an asset name in the news according to sentiment191192Args:193sentiment (str): Sentiment class of the news194share (object): A particular share to handle match for195account_id (str): An account ID to handle match for196news_id (int): ID of the news which triggered the match197quantity (int, optional): A quantity of shares to buy (Selling all always). Defaults to 1.198199Returns:200schemas.PostOrderResponse: Response from Tinkoff API or None if no order placed201"""202203logger.info("Handling match for {}".format(share.name))204205response = None206order = _get_current_order(share, account_id)207if order:208logger.warning("There is already an order for {}".format(share.name))209return response210211if sentiment == 'positive':212balance = _get_balance(account_id)213price = _get_best_price(share, schemas.OrderDirection.ORDER_DIRECTION_BUY)214215if price == 0.0: # no orders in order book216return response217218if balance*(1 + FEE) > price: # if we have enough money to buy219response = _post_order(share.figi, 1, _float_to_quotation(price), schemas.OrderDirection.ORDER_DIRECTION_BUY, account_id, schemas.OrderType.ORDER_TYPE_MARKET, news_id)220221elif sentiment == 'negative':222position = _get_position_from_account(share, account_id)223if position: # if we have a position in our portfolio224price = _get_best_price(share, schemas.OrderDirection.ORDER_DIRECTION_SELL)225226if price == 0.0: # no orders in order book227return response228229response = _post_order(share.figi, quantity, _float_to_quotation(price), schemas.OrderDirection.ORDER_DIRECTION_SELL, account_id, schemas.OrderType.ORDER_TYPE_MARKET, news_id)230231return response232233234def trade(tickers: list, account_id: str):235"""Main function for searching occurrences of assets in the news and trading them236237Args:238tickers (list): Tickers to search for239account_id (str): Account ID to trade with240"""241news = _db.select_not_checked()242shares = _get_shares_by(tickers)243244for n in news:245for share in shares:246if share.name.lower() in n['title'].lower(): # just dummy check, should be more accurate247try:248_handle_match(sentiment=n['sentiment'], share=share, account_id=account_id, news_id=n['news_id'])249except Exception as e:250logger.error(str(e))251_db.update_is_checked(n['news_id'])252253254255