Created
July 8, 2026 10:23
-
-
Save leosh1d/83459a8b2ae87186abc00df408d7b8f9 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "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