Skip to content

Instantly share code, notes, and snippets.

@leosh1d
Created July 8, 2026 10:23
Show Gist options
  • Select an option

  • Save leosh1d/83459a8b2ae87186abc00df408d7b8f9 to your computer and use it in GitHub Desktop.

Select an option

Save leosh1d/83459a8b2ae87186abc00df408d7b8f9 to your computer and use it in GitHub Desktop.
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "markdown",
"id": "e52b83d9",
"metadata": {},
"source": [
"# ReAct agent на langChain\n",
"\n",
"Напишем аналог Cursor. Агент с инструментами по взаимодействию с кодовой базой.\n",
"будут следующие тулзы: \n",
"- чтение директории (ls)\n",
"- поиск файла (fd)\n",
"- поиск внутри файлов (rg)\n",
"\n",
"\n",
"### ❗️ Важно\n",
"Для работы агента нужно чтобы все вышеперечисленные утилиты были установлены. На macos это можно делать например через brew. "
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4e2be0d6",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Requirement already satisfied: langchain-core in /opt/homebrew/anaconda3/lib/python3.13/site-packages (1.2.20)\n",
"Requirement already satisfied: langchain-openai in /opt/homebrew/anaconda3/lib/python3.13/site-packages (1.1.11)\n",
"Requirement already satisfied: langgraph in /opt/homebrew/anaconda3/lib/python3.13/site-packages (1.1.3)\n",
"Requirement already satisfied: jsonpatch<2.0.0,>=1.33.0 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from langchain-core) (1.33)\n",
"Requirement already satisfied: langsmith<1.0.0,>=0.3.45 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from langchain-core) (0.7.22)\n",
"Requirement already satisfied: packaging>=23.2.0 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from langchain-core) (24.2)\n",
"Requirement already satisfied: pydantic<3.0.0,>=2.7.4 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from langchain-core) (2.11.7)\n",
"Requirement already satisfied: pyyaml<7.0.0,>=5.3.0 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from langchain-core) (6.0.2)\n",
"Requirement already satisfied: tenacity!=8.4.0,<10.0.0,>=8.1.0 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from langchain-core) (9.0.0)\n",
"Requirement already satisfied: typing-extensions<5.0.0,>=4.7.0 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from langchain-core) (4.15.0)\n",
"Requirement already satisfied: uuid-utils<1.0,>=0.12.0 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from langchain-core) (0.14.1)\n",
"Requirement already satisfied: jsonpointer>=1.9 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from jsonpatch<2.0.0,>=1.33.0->langchain-core) (2.1)\n",
"Requirement already satisfied: httpx<1,>=0.23.0 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from langsmith<1.0.0,>=0.3.45->langchain-core) (0.28.1)\n",
"Requirement already satisfied: orjson>=3.9.14 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from langsmith<1.0.0,>=0.3.45->langchain-core) (3.11.7)\n",
"Requirement already satisfied: requests-toolbelt>=1.0.0 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from langsmith<1.0.0,>=0.3.45->langchain-core) (1.0.0)\n",
"Requirement already satisfied: requests>=2.0.0 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from langsmith<1.0.0,>=0.3.45->langchain-core) (2.32.3)\n",
"Requirement already satisfied: xxhash>=3.0.0 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from langsmith<1.0.0,>=0.3.45->langchain-core) (3.6.0)\n",
"Requirement already satisfied: zstandard>=0.23.0 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from langsmith<1.0.0,>=0.3.45->langchain-core) (0.23.0)\n",
"Requirement already satisfied: anyio in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from httpx<1,>=0.23.0->langsmith<1.0.0,>=0.3.45->langchain-core) (4.7.0)\n",
"Requirement already satisfied: certifi in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from httpx<1,>=0.23.0->langsmith<1.0.0,>=0.3.45->langchain-core) (2025.4.26)\n",
"Requirement already satisfied: httpcore==1.* in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from httpx<1,>=0.23.0->langsmith<1.0.0,>=0.3.45->langchain-core) (1.0.9)\n",
"Requirement already satisfied: idna in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from httpx<1,>=0.23.0->langsmith<1.0.0,>=0.3.45->langchain-core) (3.7)\n",
"Requirement already satisfied: h11>=0.16 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from httpcore==1.*->httpx<1,>=0.23.0->langsmith<1.0.0,>=0.3.45->langchain-core) (0.16.0)\n",
"Requirement already satisfied: annotated-types>=0.6.0 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from pydantic<3.0.0,>=2.7.4->langchain-core) (0.6.0)\n",
"Requirement already satisfied: pydantic-core==2.33.2 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from pydantic<3.0.0,>=2.7.4->langchain-core) (2.33.2)\n",
"Requirement already satisfied: typing-inspection>=0.4.0 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from pydantic<3.0.0,>=2.7.4->langchain-core) (0.4.0)\n",
"Requirement already satisfied: openai<3.0.0,>=2.26.0 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from langchain-openai) (2.29.0)\n",
"Requirement already satisfied: tiktoken<1.0.0,>=0.7.0 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from langchain-openai) (0.12.0)\n",
"Requirement already satisfied: distro<2,>=1.7.0 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from openai<3.0.0,>=2.26.0->langchain-openai) (1.9.0)\n",
"Requirement already satisfied: jiter<1,>=0.10.0 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from openai<3.0.0,>=2.26.0->langchain-openai) (0.13.0)\n",
"Requirement already satisfied: sniffio in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from openai<3.0.0,>=2.26.0->langchain-openai) (1.3.0)\n",
"Requirement already satisfied: tqdm>4 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from openai<3.0.0,>=2.26.0->langchain-openai) (4.67.1)\n",
"Requirement already satisfied: regex>=2022.1.18 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from tiktoken<1.0.0,>=0.7.0->langchain-openai) (2024.11.6)\n",
"Requirement already satisfied: langgraph-checkpoint<5.0.0,>=2.1.0 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from langgraph) (4.0.1)\n",
"Requirement already satisfied: langgraph-prebuilt<1.1.0,>=1.0.8 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from langgraph) (1.0.8)\n",
"Requirement already satisfied: langgraph-sdk<0.4.0,>=0.3.0 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from langgraph) (0.3.12)\n",
"Requirement already satisfied: ormsgpack>=1.12.0 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from langgraph-checkpoint<5.0.0,>=2.1.0->langgraph) (1.12.2)\n",
"Requirement already satisfied: charset-normalizer<4,>=2 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from requests>=2.0.0->langsmith<1.0.0,>=0.3.45->langchain-core) (3.3.2)\n",
"Requirement already satisfied: urllib3<3,>=1.21.1 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from requests>=2.0.0->langsmith<1.0.0,>=0.3.45->langchain-core) (2.3.0)\n"
]
}
],
"source": [
"%pip install langchain-core langchain-openai langgraph"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "0b820d96",
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"import subprocess\n",
"import httpx\n",
"from pathlib import Path\n",
"\n",
"from langchain_core.tools import tool\n",
"from langchain_openai import ChatOpenAI\n",
"from langgraph.prebuilt import create_react_agent\n",
"\n",
"http = httpx.Client(verify=False) # чтоб не ругались проверки апишные"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "f3250a13",
"metadata": {},
"outputs": [],
"source": [
"# хихи жжем корпоративные токены\n",
"\n",
"llm = ChatOpenAI(\n",
" openai_api_base=\"https://api.eliza.yandex.net/raw/openai/v1\",\n",
" openai_api_key=os.getenv(\"ELIZA_API_KEY\"),\n",
" model_name=\"gpt-4.1-mini\",\n",
" http_client=http,\n",
")"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "28e24c11",
"metadata": {},
"outputs": [],
"source": [
"# Константы\n",
"\n",
"CMD_TIMEOUT = 60\n",
"WORKSPACE = Path.cwd()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "db2f08af",
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"/var/folders/v8/lqntntc15fgctlxw4wdhwhx80000gn/T/ipykernel_42651/4111457762.py:67: LangGraphDeprecatedSinceV10: create_react_agent has been moved to `langchain.agents`. Please update your import to `from langchain.agents import create_agent`. Deprecated in LangGraph V1.0 to be removed in V2.0.\n",
" agent = create_react_agent(llm, tools)\n"
]
}
],
"source": [
"def _safe_resolve(relative: str) -> Path | None:\n",
" \"\"\"Чтобы не вылетить из директории\"\"\"\n",
" if relative in (\"\", \".\", \"./\"):\n",
" return WORKSPACE\n",
" candidate = (WORKSPACE / relative).resolve()\n",
" try:\n",
" candidate.relative_to(WORKSPACE)\n",
" except ValueError:\n",
" return None\n",
" return candidate\n",
"\n",
"def _run_cmd(args: list[str], cwd: Path | None = None) -> str:\n",
" try:\n",
" r = subprocess.run(\n",
" args,\n",
" cwd=str(cwd or WORKSPACE),\n",
" capture_output=True,\n",
" text=True,\n",
" timeout=CMD_TIMEOUT,\n",
" )\n",
" out = (r.stdout or \"\").strip()\n",
" err = (r.stderr or \"\").strip()\n",
" if r.returncode != 0 and not out:\n",
" return err or f\"exit {r.returncode}\"\n",
" if err and r.returncode != 0:\n",
" return f\"{out}\\n{err}\".strip() if out else err\n",
" return out or \"(пусто)\"\n",
" except subprocess.TimeoutExpired:\n",
" return f\"ошибка таймаут {CMD_TIMEOUT}с.\"\n",
"\n",
"\n",
"@tool\n",
"def ls_directory(path: str = \".\") -> str:\n",
" \"\"\"Показать содержимое директории (как ls -la). path — относительно корня воркспейса.\"\"\"\n",
" base = _safe_resolve(path)\n",
" if base is None:\n",
" return \"Ошибка: путь вне воркспейса\"\n",
" if not base.is_dir():\n",
" return f\"Не директория: {path}\"\n",
" return _run_cmd([\"ls\", \"-la\", str(base)])\n",
"\n",
"\n",
"@tool\n",
"def find_files(pattern: str, path: str = \".\") -> str:\n",
" \"\"\"Поиск файлов по имени/шаблону через fd. pattern — glob или regex в стиле fd; path — поддиректория.\"\"\"\n",
" base = _safe_resolve(path)\n",
" if base is None:\n",
" return \"Ошибка: путь вне воркспейса\"\n",
" return _run_cmd([\"fd\", pattern, str(base), \"--hidden\", \"--no-ignore-vcs\"])\n",
"\n",
"\n",
"@tool\n",
"def grep_code(pattern: str, path: str = \".\") -> str:\n",
" \"\"\"Поиск текста внутри файлов через ripgrep (rg). pattern — regex; path — файл или директория.\"\"\"\n",
" base = _safe_resolve(path)\n",
" if base is None:\n",
" return \"Ошибка: путь вышел из воркспейса\"\n",
" if not base.exists():\n",
" return f\"Не существует: {path}\"\n",
" return _run_cmd(\n",
" [\"rg\", \"--line-number\", \"--max-count\", \"150\", pattern, str(base)]\n",
" )\n",
"\n",
"\n",
"@tool\n",
"def sum(a: int, b: int) -> int:\n",
" \"\"\"Суммирует два числа\"\"\"\n",
" return a + b\n",
"\n",
"\n",
"tools = [ls_directory, find_files, grep_code, sum]\n",
"\n",
"agent = create_react_agent(llm, tools)"
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "469cdeee",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"================================\u001b[1m Human Message \u001b[0m=================================\n",
"\n",
"Какие файлы лежат в корне проекта?\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" ls_directory (call_klLyiC6jEU07Z2ZGz3F9ZypW)\n",
" Call ID: call_klLyiC6jEU07Z2ZGz3F9ZypW\n",
" Args:\n",
" path: ./\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"Name: ls_directory\n",
"\n",
"total 88\n",
"drwxr-xr-x@ 5 leosh1d staff 160 Mar 22 14:37 \u001b[1m\u001b[36m.\u001b[m\u001b[m\n",
"drwxr-xr-x@ 3 leosh1d staff 96 Mar 22 12:32 \u001b[1m\u001b[36m..\u001b[m\u001b[m\n",
"drwxr-xr-x@ 9 leosh1d staff 288 Mar 23 14:12 \u001b[1m\u001b[36m.git\u001b[m\u001b[m\n",
"-rw-r--r--@ 1 leosh1d staff 0 Mar 22 14:13 README.md\n",
"-rw-r--r--@ 1 leosh1d staff 44383 Mar 23 14:16 agents.ipynb\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"В корне проекта лежат следующие файлы и папки:\n",
"- Папка .git\n",
"- Файл README.md\n",
"- Файл agents.ipynb\n"
]
}
],
"source": [
"question = \"Какие файлы лежат в корне проекта?\"\n",
"result = agent.invoke({\"messages\": [(\"user\", question)]})\n",
"for m in result[\"messages\"]:\n",
" m.pretty_print()"
]
},
{
"cell_type": "markdown",
"id": "96a4b2bf",
"metadata": {},
"source": [
"# Агент на langGraph\n",
"\n",
"Будем через апи мосбиржи получать котировки и фигачить ии анализ. Такой железный трейдер болван \n",
"\n",
"Пока что будет пилить аналитику на неделю, эдакий админ тг канала с сигналами."
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "bc56b158",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Requirement already satisfied: apimoex in /opt/homebrew/anaconda3/lib/python3.13/site-packages (1.4.0)\n",
"Requirement already satisfied: pandas in /opt/homebrew/anaconda3/lib/python3.13/site-packages (2.2.3)\n",
"Requirement already satisfied: numpy in /opt/homebrew/anaconda3/lib/python3.13/site-packages (2.1.3)\n",
"Requirement already satisfied: requests>=2.31.0 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from apimoex) (2.32.3)\n",
"Requirement already satisfied: python-dateutil>=2.8.2 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from pandas) (2.9.0.post0)\n",
"Requirement already satisfied: pytz>=2020.1 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from pandas) (2024.1)\n",
"Requirement already satisfied: tzdata>=2022.7 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from pandas) (2025.2)\n",
"Requirement already satisfied: six>=1.5 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from python-dateutil>=2.8.2->pandas) (1.17.0)\n",
"Requirement already satisfied: charset-normalizer<4,>=2 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from requests>=2.31.0->apimoex) (3.3.2)\n",
"Requirement already satisfied: idna<4,>=2.5 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from requests>=2.31.0->apimoex) (3.7)\n",
"Requirement already satisfied: urllib3<3,>=1.21.1 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from requests>=2.31.0->apimoex) (2.3.0)\n",
"Requirement already satisfied: certifi>=2017.4.17 in /opt/homebrew/anaconda3/lib/python3.13/site-packages (from requests>=2.31.0->apimoex) (2025.4.26)\n"
]
}
],
"source": [
"!pip install apimoex pandas numpy"
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "72963e61",
"metadata": {},
"outputs": [],
"source": [
"import io\n",
"from datetime import date, datetime, timedelta\n",
"import re\n",
"from typing import Annotated, TypedDict\n",
"\n",
"import apimoex\n",
"import pandas as pd\n",
"import requests\n",
"from langchain_core.messages import AIMessage, AnyMessage, HumanMessage, SystemMessage\n",
"from langgraph.graph import END, START, StateGraph, add_messages\n",
"from pydantic import BaseModel, Field\n"
]
},
{
"cell_type": "code",
"execution_count": 17,
"id": "2c94c944",
"metadata": {},
"outputs": [],
"source": [
"# собираем константы\n",
"\n",
"class TraderIntent(BaseModel):\n",
" ticker: str | None = Field(\n",
" default=None,\n",
" description=\"Тикер MOEX TQBR латиницей; русские названия сопоставь с тикером.\",\n",
" )\n",
" date_start: str | None = Field(\n",
" default=None,\n",
" description=\"Начало интервала свечей ГГГГ-ММ-ДД по смыслу запроса; если период не указан — null.\",\n",
" )\n",
" date_end: str | None = Field(\n",
" default=None,\n",
" description=\"Конец интервала ГГГГ-ММ-ДД включительно; если период не указан — null.\",\n",
" )\n",
"\n",
"\n",
"class MoexTraderState(TypedDict):\n",
" messages: Annotated[list[AnyMessage], add_messages]\n",
" ticker: str | None\n",
" date_start: str | None\n",
" date_end: str | None\n",
" quotes_text: str | None\n",
"\n",
"\n",
"_moex_session = requests.Session()\n",
"\n",
"CANDLES_INTERVAL = 24\n",
"\n",
"TRADER_SYSTEM = \"\"\"Ты опытный трейдер по российским акциям. По дневным свечам за интервал из данных дай сжатый разбор.\n",
"Язык: русский. Без рекомендаций «покупай/продавай».\n",
"Пункты: тренд и волатильность; уровни; объёмы; риски и что смотреть дальше.\n",
"\"\"\""
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4892c753",
"metadata": {},
"outputs": [],
"source": [
"# Утилитки\n",
"\n",
"def _parse_iso(d):\n",
" if not d:\n",
" return None\n",
" s = str(d).strip()[:10]\n",
" try:\n",
" return datetime.strptime(s, \"%Y-%m-%d\").date()\n",
" except ValueError:\n",
" return None\n",
"\n",
"\n",
"def _default_range():\n",
" end = date.today()\n",
" return end - timedelta(days=7), end\n",
"\n",
"\n",
"def _coalesce_range(date_start, date_end):\n",
" if date_start is None and date_end is None:\n",
" return _default_range()\n",
" if date_start is None:\n",
" date_end = date_end or date.today()\n",
" return date_end - timedelta(days=7), date_end\n",
" if date_end is None:\n",
" return date_start, date.today()\n",
" if date_end < date_start:\n",
" date_start, date_end = date_end, date_start\n",
" return date_start, date_end\n",
"\n",
"\n",
"def _last_human_content(messages):\n",
" for m in reversed(messages):\n",
" if isinstance(m, HumanMessage):\n",
" raw = m.content\n",
" if raw is None:\n",
" return None\n",
" text = str(raw).strip()\n",
" return text if text else None\n",
" return None\n",
"\n",
"\n",
"def _fetch_candles(security, start, end):\n",
" sec = security.upper().strip()\n",
" raw = apimoex.get_board_candles(\n",
" _moex_session,\n",
" security=sec,\n",
" interval=CANDLES_INTERVAL,\n",
" start=start.isoformat(),\n",
" end=end.isoformat(),\n",
" board=\"TQBR\",\n",
" )\n",
" df = pd.DataFrame(raw)\n",
" if df.empty:\n",
" return f\"По тикеру {sec} за {start}…{end} данных нет.\"\n",
" for col in (\"open\", \"close\", \"high\", \"low\", \"volume\", \"value\"):\n",
" if col in df.columns:\n",
" df[col] = pd.to_numeric(df[col], errors=\"coerce\")\n",
" buf = io.StringIO()\n",
" df.to_csv(buf, index=False)\n",
" text = buf.getvalue()\n",
" lines = [f\"Тикер {sec}, TQBR, дневные свечи, {start}…{end}.\"]\n",
" if len(df) >= 2 and \"close\" in df.columns:\n",
" c0, c1 = float(df[\"close\"].iloc[0]), float(df[\"close\"].iloc[-1])\n",
" lines.append(f\"Изменение close: {(c1 / c0 - 1) * 100:+.2f}%.\")\n",
" if \"high\" in df.columns and \"low\" in df.columns:\n",
" lines.append(\n",
" f\"Диапазон low…ahigh: {df['low'].min():.2f} … {df['high'].max():.2f}.\"\n",
" )\n",
" if \"volume\" in df.columns:\n",
" lines.append(f\"Суммарный объём: {df['volume'].sum():,.0f} шт.\")\n",
" return \"\\n\".join(lines) + \"\\n\\n\" + text"
]
},
{
"cell_type": "code",
"execution_count": 19,
"id": "ef15553f",
"metadata": {},
"outputs": [],
"source": [
"# Слои\n",
"\n",
"def resolve_intent(state):\n",
" last_human = _last_human_content(state[\"messages\"])\n",
" today = date.today().isoformat()\n",
" if not last_human:\n",
" s, e = _default_range()\n",
" return {\"ticker\": None, \"date_start\": s.isoformat(), \"date_end\": e.isoformat()}\n",
" out = llm.with_structured_output(TraderIntent).invoke(\n",
" [\n",
" SystemMessage(\n",
" content=(\n",
" f\"Сегодня {today} (ориентир для относительных дат). \"\n",
" \"Извлеки тикер акции Мосбиржи (TQBR) и границы интервала дневных свечей. \"\n",
" \"Пользователь может писать по-русски («Сбер», «за прошлый месяц», «с 1 по 20 марта»). \"\n",
" \"Если период в запросе не фигурирует — оставь date_start и date_end пустыми (null). \"\n",
" \"Если бумагу нельзя определить — ticker=null.\"\n",
" )\n",
" ),\n",
" HumanMessage(content=last_human),\n",
" ]\n",
" )\n",
" t = (out.ticker or \"\").strip().upper() if out.ticker else None\n",
" if not t or not re.fullmatch(r\"[A-Z][A-Z0-9]{0,11}\", t):\n",
" t = None\n",
" date_start = _parse_iso(out.date_start)\n",
" date_end = _parse_iso(out.date_end)\n",
" date_start, date_end = _coalesce_range(date_start, date_end)\n",
" return {\"ticker\": t, \"date_start\": date_start.isoformat(), \"date_end\": date_end.isoformat()}\n",
"\n",
"\n",
"def route_after_resolve(state):\n",
" return \"fetch_quotes\" if state.get(\"ticker\") else \"need_ticker\"\n",
"\n",
"\n",
"def need_ticker(state):\n",
" return {\n",
" \"messages\": [\n",
" AIMessage(\n",
" content=\"Не понял, какую бумагу разбирать. Укажи тикер (например SBER) или название компании.\"\n",
" )\n",
" ]\n",
" }\n",
"\n",
"\n",
"def fetch_quotes(state):\n",
" t = state.get(\"ticker\")\n",
" if not t:\n",
" return {\"quotes_text\": \"Тикер не задан.\"}\n",
" date_start = _parse_iso(state.get(\"date_start\"))\n",
" date_end = _parse_iso(state.get(\"date_end\"))\n",
" date_start, date_end = _coalesce_range(date_start, date_end)\n",
" try:\n",
" text = _fetch_candles(t, date_start, date_end)\n",
" except requests.RequestException as e:\n",
" text = f\"Ошибка сети MOEX: {e!r}\"\n",
" except Exception as e:\n",
" text = f\"Ошибка MOEX: {e!r}\"\n",
" return {\"quotes_text\": text}\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "731c324a",
"metadata": {},
"outputs": [],
"source": [
"# собираем\n",
"\n",
"def analyze(state):\n",
" msg = llm.invoke(\n",
" [\n",
" SystemMessage(content=TRADER_SYSTEM),\n",
" HumanMessage(\n",
" content=(\n",
" f\"Тикер: {state.get('ticker')}\"\n",
" f\"Интервал: {state.get('date_start')} … {state.get('date_end')}\"\n",
" f\"{state.get('quotes_text') or ''}\"\n",
" )\n",
" ),\n",
" ]\n",
" )\n",
" return {\"messages\": [msg]}\n",
"\n",
"\n",
"_builder = StateGraph(MoexTraderState)\n",
"_builder.add_node(\"resolve_intent\", resolve_intent)\n",
"_builder.add_node(\"need_ticker\", need_ticker)\n",
"_builder.add_node(\"fetch_quotes\", fetch_quotes)\n",
"_builder.add_node(\"analyze\", analyze)\n",
"_builder.add_edge(START, \"resolve_intent\")\n",
"_builder.add_conditional_edges(\"resolve_intent\", route_after_resolve)\n",
"_builder.add_edge(\"need_ticker\", END)\n",
"_builder.add_edge(\"fetch_quotes\", \"analyze\")\n",
"_builder.add_edge(\"analyze\", END)\n",
"\n",
"moex_trader_graph = _builder.compile()\n",
"\n",
"\n",
"def run_moex_trader_prompt(user_text):\n",
" return moex_trader_graph.invoke(\n",
" {\n",
" \"messages\": [HumanMessage(content=user_text)],\n",
" \"ticker\": None,\n",
" \"date_start\": None,\n",
" \"date_end\": None,\n",
" \"quotes_text\": None,\n",
" }\n",
" )"
]
},
{
"cell_type": "code",
"execution_count": 16,
"id": "625eb275",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"================================\u001b[1m Human Message \u001b[0m=================================\n",
"\n",
"Сделай разбор Сбера за прошлый месяц\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"Разбор по SBER за период 2026-02-01 … 2026-03-23:\n",
"\n",
"1. Тренд и волатильность \n",
"За рассматриваемый интервал цена акции показала умеренный восходящий тренд с ростом закрытия на +3.31%. Цена колебалась в диапазоне от 300.41 до 322.17 рублей, что указывает на среднюю волатильность – импульсные движения были, но без резких выбросов. В первой половине февраля была заметна консолидация и легкая коррекция вниз до уровней 300-303. Затем с середины месяца и особенно в марте акции постепенно росли.\n",
"\n",
"2. Основные уровни \n",
"- Поддержка: около 300-302 руб., уровень нескольких касаний в начале и середине февраля. \n",
"- Сопротивление: зона 317-322 руб., где цена встречала затруднения при попытках роста в конце марта. \n",
"- Текущий уровень (на конец периода) — 316.51 руб., немного ниже локального максимума марта (322.17).\n",
"\n",
"3. Объёмы \n",
"Объёмы торгов были значительными, суммарно более 813 млн акций. Высокие объёмы отмечены при росте цены в середине и конце марта (до 38 млн в отдельные дни), что подтверждает интерес участников рынка при пробоях выше 315 руб. В периоды боковика и коррекции объемы снижались, что типично для фазы накопления.\n",
"\n",
"4. Риски и последующие ожидания для наблюдения \n",
"- Возможен отскок от уровня поддержки около 314-315 руб., если цена не удержится выше, возможны более глубокие коррекции до 300 руб. \n",
"- Следить за объемами при подходе к сопротивлению 320-322 руб.: рост на снижающихся объемах может сигнализировать о слабости тренда. \n",
"- Волатильность умеренная, но возможны резкие движения при новости или внешних факторах. \n",
"- Важно отслеживать подтверждение тренда с помощью свечных формаций и динамику объёмов для оценки силы движения.\n",
"\n",
"В целом, ситуация характеризуется умеренным ростом на фоне средних объёмов с зонами ключевых уровней поддержки и сопротивления, что требует внимания к реакциям цены в указанных диапазонах.\n"
]
}
],
"source": [
"out = run_moex_trader_prompt(\"Сделай разбор Сбера за прошлый месяц\")\n",
"for m in out[\"messages\"]:\n",
" m.pretty_print()"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "base",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.5"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment