Введение
С тех пор, как я начал изучать рынок ценных бумаг у меня возникла мысль «А почему бы не автоматизировать весь процесс анализа и покупки акций на бирже?». Идея создания торгового робота не покидала меня, и вот что из этого вышло.
С ростом популярности ИИ‑агентов и фреймворков для их разработки стало целесообразно протестировать их в этой сфере в качестве помощников.
Многие уже пробовали использовать ИИ‑агентов в торговле. Например, в статье «Мы заставили ИИ‑модели торговать на бирже.» приведены результаты торгов на различных биржах, на Reddit куча обсуждений о том, могут ли агенты приносить прибыль в торговле как криптовалютой, так и ценными бумагами.
В этой статье я покажу, как ИИ‑агенты могут стать новым способом взаимодействия с биржей. Их применение может помочь привлечь новых инвесторов благодаря приятному и понятному пользователю чат‑интерфейсу.
Примечание
На скриншотах вы встретите логотипы и фирменные цвета Т‑Банка. Это сделано намеренно, так как проект строится на официальном T-Invest API и их SDK. Все материалы приведены исключительно в демонстрационных целях. Статья не является рекламой, не содержит инвестиционных рекомендаций и не подразумевает официального партнёрства с банком.
SDK T‑Банка. Инструменты для взаимодействия с биржей
Так как я давно являюсь пользователем сервисов T‑Банка, плюс ко всему у них есть удобный SDK, а также песочница, в которой можно тестировать решения, мой выбор пал на официальный T-Invest API. Готовый SDK и полноценная песочница позволили быстро собрать прототип, отработать механику выставления ордеров и безопасно тестировать сценарии без риска. Как установить и начать работать вы можете узнать из официальной документации, а в этом блоке я расскажу, как я использовал и реализовывал инструменты для агентов.
Основные инструменты в моем прототипе:
Получение данных о купленных позициях;
Получение данных о балансе;
Получение технических индикаторов, а также историю цен акции за 7 дней;
Покупка и продажа акций;
Получение наиболее популярных акций (в зависимости от оборота за сутки) по секторам экономики.
Также дополнительно были написаны инструменты по получению из smartlab новостных публикаций и комментариев их форума по тикеру.
Все это было реализовано в виде двух mcp серверов. Разберем каждый инструмент подробнее.
Получение данных о купленных позициях
@mcp.tool() def get_positions() -> str: """ Get the current positions of the account. If there is no desired position, then it has not been purchased yet. Returns: A DataFrame containing the current positions of the account. figi: The FIGI of the instrument. ticker: The ticker of the instrument. current_price: The current price of the instrument. quantity: The quantity of the instrument in the account. quantity_lots: The number of lots of the instrument on the account. average_position_price: The average price of the position in the account. """ with SandboxClient(TOKEN) as client: positions = client.operations.get_portfolio(account_id=ACCOUNT_ID).positions if len(positions) > 1: positions_df = to_df(positions) positions_df = positions_df.iloc[1:, :] return positions_df[['figi', 'ticker', 'quantity_lots', 'quantity', 'current_price' , 'average_position_price']].to_markdown() else: return 'No positions in the account.'
На выходе имеем список датаклассов PortfolioPosition с кучей атрибутов. Я не стал особо разбираться в значении каждого и отобрал для себя следующие: [‘figi’, ‘ticker’, ‘quantity_lots’, ‘quantity’, ‘current_price’ , ‘average_position_price’]. Все это красиво вывел markdown таблицей:
figi | ticker | quantity_lots | quantity | current_price | average_position_price |
|---|---|---|---|---|---|
TCS03A108X38 | X5 | 12 | 12 | 2448.500 rub | 2436.500 rub |
TCS00Y3XYV94 | MDMG | 11 | 11 | 1334.900 rub | 1362.300 rub |
BBG004S68473 | IRAO | 5 | 500 | 3.189 rub | 3.20700 rub |
BBG004730N88 | SBER | 1 | 1 | 317.600 rub | 320.400 rub |
figi - уникальный идентификатор. Подробнее можно прочесть в документации;
quantity_lots - количество лотов;
quantity - количество акций;
current_price - текущая цена акции;
average_position_price - средняя цена покупки всех акций.
Покупка и продажа акций
@mcp.tool() def buy(figi: str, quantity_lots: int) -> str: """ Buy a financial instrument. Args: figi: The FIGI identifier of the instrument to buy quantity_lots: Number of lots to purchase (MAX 200) Returns: Order execution details """ with SandboxClient(TOKEN) as client: sb = client.sandbox buy = sb.post_sandbox_order( account_id=ACCOUNT_ID, figi=figi, quantity=quantity_lots, # Количество direction=OrderDirection.ORDER_DIRECTION_BUY, #Покупка order_type=OrderType.ORDER_TYPE_MARKET # По текущей цене ) time.sleep(2) return "Done!" @mcp.tool() def sale(figi: str, quantity_lots: int) -> str: """ Execute a market sell order for a specified security in the sandbox environment. This function connects to the T-Invest Sandbox API and places a market sell order for the given FIGI (Financial Instrument Global Identifier) and quantity. Args: figi (str): The FIGI identifier of the security to sell quantity_lots (int): Number of lots to sell (MAX 200) Returns: str: "Done!" if the order was successfully executed, or an error message starting with "ERROR: " if an exception occurred """ with SandboxClient(TOKEN) as client: sb = client.sandbox buy = sb.post_sandbox_order( account_id=ACCOUNT_ID, figi=figi, quantity=quantity_lots, # Количество direction=OrderDirection.ORDER_DIRECTION_SELL, #Продажа order_type=OrderType.ORDER_TYPE_MARKET # По текущей цене ) time.sleep(2) return "Done!"
Так как я все тестировал в песочнице, проверку на то, что ордер на покупку/продажу исполнен я не выполнял и ограничился time.sleep =)
Стоит также сказать, что для всех операций с акциями используются FIGI, поэтому дополнительно реализован инструмент, который получает FIGI по тикеру:
@mcp.tool() def get_figi(input: List[str]) -> str: """ Get the FIGI identifiers for a list of tickers. Args: input: A list of ticker symbols (e.g., ["SBER", "GAZP", "LKOH"]) Returns: A string containing the ticker and its corresponding FIGI identifier, separated by a colon and a """ tikers_ret = '' for ticker in input: with SandboxClient(TOKEN) as client: share = client.instruments.share_by(id_type=InstrumentIdType.INSTRUMENT_ID_TYPE_TICKER, id=ticker, class_code="TQBR") instruments = share.instrument figi = instruments.figi tikers_ret+=f'{ticker}: {figi}\n' return tikers_ret
Получение технических индикаторов, а также историю цен акций за 7 дней
@mcp.tool() def get_analytics(figi_list: list[str]) -> str: """ Calculate technical indicators and fetch price data for a list of instruments. Retrieves RSI, SMA, EMA (14-period, 4-hour close) along with the closing price for each instrument over the last 7 days. Data is filtered to the current trading day, adjusted to UTC+3, and compared against the last processed timestamp to return only new or first-time results. Args: figi_list (list[str]): List of FIGI identifiers to analyze. Example: ["BBG004730N88", "BBG004731578"] Returns: str: Markdown-formatted output containing: - Summary header with processing statistics - Table with columns: FIGI, Timestamp, Price, RSI, SMA, EMA - Error log for any invalid or unavailable instruments """ if not figi_list: return "Parameter error: 'figi_list' must be a non-empty list of FIGI strings." now = datetime.now() week_ago = now - timedelta(days=7) indicators = [ IndicatorType.INDICATOR_TYPE_RSI, IndicatorType.INDICATOR_TYPE_SMA, IndicatorType.INDICATOR_TYPE_EMA ] successful_dfs = [] errors = [] with SandboxClient(TOKEN) as client: inst = client.market_data for figi in figi_list: try: uid = get_uid(figi) all_data = {} for indicator in indicators: request = GetTechAnalysisRequest( instrument_uid=uid, from_=week_ago, to=now, interval=IndicatorInterval.INDICATOR_INTERVAL_4_HOUR, type_of_price=TypeOfPrice.TYPE_OF_PRICE_CLOSE, indicator_type=indicator, length=14 ) response = inst.get_tech_analysis(request=request) # Получаем данные по тех. индикатору for item in response.technical_indicators: ts = item.timestamp val = item.signal.units + item.signal.nano / 1e9 if ts not in all_data: all_data[ts] = {'timestamp': ts} col_name = indicator.name.replace('INDICATOR_TYPE_', '').lower() all_data[ts][col_name] = val candles_resp = inst.get_candles( figi=figi, from_=week_ago, to=now, interval=CandleInterval.CANDLE_INTERVAL_4_HOUR ) # Получаем историю цены price_map = { c.time: c.close.units + c.close.nano / 1e9 for c in candles_resp.candles } for ts in all_data: if ts in price_map: all_data[ts]['price'] = price_map[ts] df = pd.DataFrame(list(all_data.values())).sort_values('timestamp').reset_index(drop=True) if df.empty: errors.append(f"{figi}: API returned empty technical data.") continue df['timestamp'] = df['timestamp'] + pd.Timedelta(hours=3) if figi in time_: # Проверяем делали ли анализ по акции df = df[df['timestamp'] > time_[figi]] # Берем актуальные данные if df.empty: continue time_[figi] = df['timestamp'].iloc[-1] # Заносим последнее время else: df = df[-200:] # Limit initial load if df.empty: continue time_[figi] = df['timestamp'].iloc[-1] df['figi'] = figi successful_dfs.append(df) except Exception as e: errors.append(f"{figi}: {str(e)}") if not successful_dfs: err_msg = "\n".join(f"- {e}" for e in errors) if errors else "No new or valid data found for the requested FIGIs." return f" **Analysis Complete**\n\n{err_msg}" result_df = pd.concat(successful_dfs, ignore_index=True) result_df = result_df[['figi', 'timestamp', 'price', 'rsi', 'sma', 'ema']] output = ( f"**Technical Analysis Report**\n" f"Processed: {len(figi_list)} | Success: {len(successful_dfs)} | Skipped/Errors: {len(errors)}\n\n" f"{result_df.to_markdown(index=False)}" ) if errors: output += f"\n\n**Errors/Notes:**\n" + "\n".join(f"- {e}" for e in errors) return output
Механизм с буферизацией последнего timestampсделан для того, чтобы не захламлять контекст с моделью. Очень грубо, но для прототипа пойдет.
Получения наиболее популярных акций (в зависимости от оборота за сутки) по секторам экономики
@mcp.tool() async def get_popular_shares_by_turnover(sector_names: list[str], top_n: int = 3) -> str: """ Retrieve the most popular (highly traded) shares for specified sectors based on 24h turnover. This tool calculates the real monetary turnover (Price * Volume) for each share in the given sectors to identify market leaders and the most liquid instruments. Args: sector_names (list[str]): List of sectors to analyze. Options: 'consumer', 'energy', 'financial', 'health_care', 'industrials', 'it', 'materials', 'real_estate', 'telecom', 'utilities'. top_n (int): Number of top-traded shares to return per sector. Default is 3. Returns: str: A markdown table with Ticker, Name, Sector, and 24h Turnover (RUB). Returns an error message if the API call fails or no data is found. """ if not sector_names: return "Parameter error: 'sector_names' must be a non-empty list." try: with SandboxClient(TOKEN) as client: all_shares = client.instruments.shares().instruments target_sectors = set(sector_names) filtered_shares = [ s for s in all_shares if s.class_code == 'TQBR' and s.sector in target_sectors ] if not filtered_shares: return "No shares found for the specified sector(s)." results = [] now = datetime.now() start = now - timedelta(days=1) for share in filtered_shares: try: candles = client.market_data.get_candles( figi=share.figi, from_=start, to=now, interval=CandleInterval.CANDLE_INTERVAL_HOUR ).candles turnover = sum( float(quotation_to_decimal(c.close)) * c.volume * share.lot for c in candles ) # Расчет оборота: Цена * Объем * Лотность if turnover > 0: results.append({ "ticker": share.ticker, "name": share.name, "sector": share.sector, "lot": share.lot, "turnover_rub": round(turnover, 2) }) except: continue # Пропускаем инструменты без данных if not results: return "Could not calculate turnover for these sectors at the moment." df = pd.DataFrame(results) df = df.sort_values(['sector', 'turnover_rub'], ascending=[True, False]) # Сортируем внутри каждого сектора по убыванию оборота top_df = df.groupby('sector').head(top_n) # Берем топ-N для каждого сектора return top_df.to_markdown(index=False) except Exception as e: return f"ERROR: {e}"
Фреймворки и как это все выглядело в итоге
Для реализации ассистента были использованы фреймворки Chainlit и LangChain. О каждом можно говорить много и долго. Скажу лишь, что с каждым я поработал достаточно полно. Если будет интересно - расскажу про каждый в следующих статьях. В качестве ИИ-провайдера изначально использовал openrouter с бесплатной моделью StepFun: Step 3.5 Flash, но вскоре она стала платной, поэтому использовал российский провайдер Polza.ai.
Что мне нравится в Chainlit, так это то, что можно кастомизировать интерфейс под себя:

Выглядит круто!
Разберем один из примеров использования:
Предположим, мы юзер, который вроде что-то знает об акциях, примерно понимает как это работает, но у нас нет времени на то, чтобы читать новости, форумы и анализировать индикаторы. Мы едем в метро и хотим быстро получить нужную информацию и совершить сделку.
Соответственно на экране пользователь сможет наблюдать все этапы, просмотреть какие данные получила модель.


Как итог, пользователь получит рекомендации от ассистента:

Теперь давайте попробуем приобрести акции Яндекса по рекомендациям агента:


Вывод
В этой статье был рассмотрен Кейс по применению нового вида взаимодействия пользователя с биржей.
Что удалось реализовать:
Прототип агента, который понимает естественные запросы вроде;
Интеграцию с официальным T-Invest API: получение котировок, индикаторов, исполнение ордеров и работа с портфелем;
Подключение внешних источников данных (новости и комментарии из SmartLab) для получения дополнительной информации;
Удобный чат-интерфейс на Chainlit, где пользователь видит не только ответ, но и ход мыслей агента.
Ограничения прототипа:
Тестирование проводилось в песочнице без реальных задержек и прочих внешних факторов;
Механизм буферизации контекста и обработка статусов ордеров требуют доработки для продакшена;
Пользователь остаётся ответственным за свои решения.
Куда двигаться дальше:
Добавить модули риск-менеджмента (лимиты на сделки, стоп-лоссы, диверсификация и т.д.);
Подключить Интегрировать возможность голосового ввода для ещё более естественного взаимодействия;
Проработать сценарии аргументации действий (почему агент предложил именно эту сделку).
Важно понимать ИИ-агенты не заменят инвестора, но могут стать надёжным инструментом и взять на себя исполнение рутинных действий, таких как сбор и анализ информации, мониторинг рынка и т.д. Такой подход не только повышает эффективность, но и меняет саму парадигму взаимодействия с биржей, делая её доступнее для тех, кто раньше откладывал инвестиции из-за сложного интерфейса или высокого порога входа.
Отдельная благодарность команде Т-Банка за открытый T-Invest API и песочницу! Именно такие инструменты позволяют разработчикам безопасно экспериментировать и превращать идеи в реальность.
Проект открыт для коллаборации: желающим присоединиться к развитию и доработки пишите в личные сообщения.
Интересно узнать в какие сферы вы пробовали внедрить ИИ-агентов? Какие, на ваш взгляд, сценарии должны быть у ИИ-инвестора?
Делитесь мнением о том, какие сценарии автоматизации имеют наибольший потенциал.