diff --git a/.env.example b/.env.example index 3bc855514..bbebef85a 100644 --- a/.env.example +++ b/.env.example @@ -21,3 +21,8 @@ FINANCIAL_DATASETS_API_KEY=your-financial-datasets-api-key # For running LLMs hosted by openai (gpt-4o, gpt-4o-mini, etc.) # Get your OpenAI API key from https://platform.openai.com/ OPENAI_API_KEY=your-openai-api-key + +# Alpaca (required) +ALPACA_API_KEY=your-alpaca-api-key +ALPACA_SECRET_KEY=your-alpaca-secret-key +ALPACA_PAPER=true # Set to false for live trading diff --git a/poetry.lock b/poetry.lock index 151478058..891b3a263 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -6,6 +6,7 @@ version = "2.6.1" description = "Happy Eyeballs for asyncio" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}, {file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"}, @@ -17,6 +18,7 @@ version = "3.11.18" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "aiohttp-3.11.18-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:96264854fedbea933a9ca4b7e0c745728f01380691687b7365d18d9e977179c4"}, {file = "aiohttp-3.11.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9602044ff047043430452bc3a2089743fa85da829e6fc9ee0025351d66c332b6"}, @@ -111,7 +113,7 @@ propcache = ">=0.2.0" yarl = ">=1.17.0,<2.0" [package.extras] -speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"] +speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns (>=3.2.0) ; sys_platform == \"linux\" or sys_platform == \"darwin\"", "brotlicffi ; platform_python_implementation != \"CPython\""] [[package]] name = "aiosignal" @@ -119,6 +121,7 @@ version = "1.3.2" description = "aiosignal: a list of registered asynchronous callbacks" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5"}, {file = "aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54"}, @@ -133,6 +136,7 @@ version = "1.15.2" description = "A database migration tool for SQLAlchemy." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "alembic-1.15.2-py3-none-any.whl", hash = "sha256:2e76bd916d547f6900ec4bb5a90aeac1485d2c92536923d0b138c02b126edc53"}, {file = "alembic-1.15.2.tar.gz", hash = "sha256:1c72391bbdeffccfe317eefba686cb9a3c078005478885413b95c3b26c57a8a7"}, @@ -146,12 +150,33 @@ typing-extensions = ">=4.12" [package.extras] tz = ["tzdata"] +[[package]] +name = "alpaca-py" +version = "0.40.2" +description = "The Official Python SDK for Alpaca APIs" +optional = false +python-versions = "<4.0.0,>=3.8.0" +groups = ["main"] +files = [ + {file = "alpaca_py-0.40.2-py3-none-any.whl", hash = "sha256:bd21a5d290051d28ff4811b0cda2a1b45a4c6bb80f49b037c7bc2fe15302f035"}, + {file = "alpaca_py-0.40.2.tar.gz", hash = "sha256:d1e63b628cff4d9935a0c24c3a4419a985d4216a531c5c7b2d9fcd49e3c5fb70"}, +] + +[package.dependencies] +msgpack = ">=1.0.3,<2.0.0" +pandas = ">=1.5.3" +pydantic = ">=2.0.3,<3.0.0" +requests = ">=2.30.0,<3.0.0" +sseclient-py = ">=1.7.2,<2.0.0" +websockets = ">=10.4" + [[package]] name = "annotated-types" version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, @@ -163,6 +188,7 @@ version = "0.50.0" description = "The official Python library for the anthropic API" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "anthropic-0.50.0-py3-none-any.whl", hash = "sha256:defbd79327ca2fa61fd7b9eb2f1627dfb1f69c25d49288c52e167ddb84574f80"}, {file = "anthropic-0.50.0.tar.gz", hash = "sha256:42175ec04ce4ff2fa37cd436710206aadff546ee99d70d974699f59b49adc66f"}, @@ -187,6 +213,7 @@ version = "3.7.1" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, @@ -198,7 +225,7 @@ sniffio = ">=1.1" [package.extras] doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"] -test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4) ; python_version < \"3.8\"", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17) ; python_version < \"3.12\" and platform_python_implementation == \"CPython\" and platform_system != \"Windows\""] trio = ["trio (<0.22)"] [[package]] @@ -207,18 +234,19 @@ version = "25.3.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, ] [package.extras] -benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] -tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] +tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] [[package]] name = "black" @@ -226,6 +254,7 @@ version = "23.12.1" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "black-23.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2"}, {file = "black-23.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba"}, @@ -260,7 +289,7 @@ platformdirs = ">=2" [package.extras] colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] +d = ["aiohttp (>=3.7.4) ; sys_platform != \"win32\" or implementation_name != \"pypy\"", "aiohttp (>=3.7.4,!=3.9.0) ; sys_platform == \"win32\" and implementation_name == \"pypy\""] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] @@ -270,6 +299,7 @@ version = "5.5.2" description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a"}, {file = "cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4"}, @@ -281,6 +311,7 @@ version = "2025.4.26" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3"}, {file = "certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6"}, @@ -292,6 +323,7 @@ version = "3.4.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, @@ -393,6 +425,7 @@ version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] files = [ {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, @@ -407,6 +440,7 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -418,6 +452,7 @@ version = "1.3.2" description = "Python library for calculating contours of 2D quadrilateral grids" optional = false python-versions = ">=3.10" +groups = ["main"] files = [ {file = "contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934"}, {file = "contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989"}, @@ -494,6 +529,7 @@ version = "0.12.1" description = "Composable style cycles" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"}, {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"}, @@ -509,6 +545,7 @@ version = "0.7.1" description = "XML bomb protection for Python stdlib modules" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["main"] files = [ {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, @@ -520,6 +557,7 @@ version = "1.9.0" description = "Distro - an OS platform information API" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, @@ -531,6 +569,7 @@ version = "0.104.1" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "fastapi-0.104.1-py3-none-any.whl", hash = "sha256:752dc31160cdbd0436bb93bad51560b57e525cbb1d4bbf6f4904ceee75548241"}, {file = "fastapi-0.104.1.tar.gz", hash = "sha256:e5e4540a7c5e1dcfbbcf5b903c234feddcdcd881f191977a1c5dfd917487e7ae"}, @@ -551,6 +590,7 @@ version = "0.0.7" description = "Run and manage FastAPI apps from the command line with FastAPI CLI. 🚀" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "fastapi_cli-0.0.7-py3-none-any.whl", hash = "sha256:d549368ff584b2804336c61f192d86ddea080c11255f375959627911944804f4"}, {file = "fastapi_cli-0.0.7.tar.gz", hash = "sha256:02b3b65956f526412515907a0793c9094abd4bfb5457b389f645b0ea6ba3605e"}, @@ -570,6 +610,7 @@ version = "1.2.0" description = "Infer file type and MIME type of any file/buffer. No external dependencies." optional = false python-versions = "*" +groups = ["main"] files = [ {file = "filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25"}, {file = "filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb"}, @@ -581,6 +622,7 @@ version = "6.1.0" description = "the modular source code checker: pep8 pyflakes and co" optional = false python-versions = ">=3.8.1" +groups = ["dev"] files = [ {file = "flake8-6.1.0-py2.py3-none-any.whl", hash = "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5"}, {file = "flake8-6.1.0.tar.gz", hash = "sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23"}, @@ -597,6 +639,7 @@ version = "4.57.0" description = "Tools to manipulate font files" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "fonttools-4.57.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:babe8d1eb059a53e560e7bf29f8e8f4accc8b6cfb9b5fd10e485bde77e71ef41"}, {file = "fonttools-4.57.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81aa97669cd726349eb7bd43ca540cf418b279ee3caba5e2e295fb4e8f841c02"}, @@ -651,18 +694,18 @@ files = [ ] [package.extras] -all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "pycairo", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0)", "xattr", "zopfli (>=0.1.4)"] +all = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "fs (>=2.2.0,<3)", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres ; platform_python_implementation == \"PyPy\"", "pycairo", "scipy ; platform_python_implementation != \"PyPy\"", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0) ; python_version <= \"3.12\"", "xattr ; sys_platform == \"darwin\"", "zopfli (>=0.1.4)"] graphite = ["lz4 (>=1.7.4.2)"] -interpolatable = ["munkres", "pycairo", "scipy"] +interpolatable = ["munkres ; platform_python_implementation == \"PyPy\"", "pycairo", "scipy ; platform_python_implementation != \"PyPy\""] lxml = ["lxml (>=4.0)"] pathops = ["skia-pathops (>=0.5.0)"] plot = ["matplotlib"] repacker = ["uharfbuzz (>=0.23.0)"] symfont = ["sympy"] -type1 = ["xattr"] +type1 = ["xattr ; sys_platform == \"darwin\""] ufo = ["fs (>=2.2.0,<3)"] -unicode = ["unicodedata2 (>=15.1.0)"] -woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] +unicode = ["unicodedata2 (>=15.1.0) ; python_version <= \"3.12\""] +woff = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "zopfli (>=0.1.4)"] [[package]] name = "frozenlist" @@ -670,6 +713,7 @@ version = "1.6.0" description = "A list-like structure which implements collections.abc.MutableSequence" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "frozenlist-1.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e6e558ea1e47fd6fa8ac9ccdad403e5dd5ecc6ed8dda94343056fa4277d5c65e"}, {file = "frozenlist-1.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f4b3cd7334a4bbc0c472164f3744562cb72d05002cc6fcf58adb104630bbc352"}, @@ -783,6 +827,7 @@ version = "0.6.18" description = "Google Ai Generativelanguage API client library" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "google_ai_generativelanguage-0.6.18-py3-none-any.whl", hash = "sha256:13d8174fea90b633f520789d32df7b422058fd5883b022989c349f1017db7fcf"}, {file = "google_ai_generativelanguage-0.6.18.tar.gz", hash = "sha256:274ba9fcf69466ff64e971d565884434388e523300afd468fc8e3033cd8e606e"}, @@ -792,7 +837,7 @@ files = [ google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0", extras = ["grpc"]} google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0" proto-plus = [ - {version = ">=1.22.3,<2.0.0", markers = "python_version < \"3.13\""}, + {version = ">=1.22.3,<2.0.0"}, {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""}, ] protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" @@ -803,6 +848,7 @@ version = "2.24.2" description = "Google API client core library" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "google_api_core-2.24.2-py3-none-any.whl", hash = "sha256:810a63ac95f3c441b7c0e43d344e372887f62ce9071ba972eacf32672e072de9"}, {file = "google_api_core-2.24.2.tar.gz", hash = "sha256:81718493daf06d96d6bc76a91c23874dbf2fac0adbbf542831b805ee6e974696"}, @@ -814,7 +860,7 @@ googleapis-common-protos = ">=1.56.2,<2.0.0" grpcio = {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""} grpcio-status = {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""} proto-plus = [ - {version = ">=1.22.3,<2.0.0", markers = "python_version < \"3.13\""}, + {version = ">=1.22.3,<2.0.0"}, {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""}, ] protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" @@ -822,7 +868,7 @@ requests = ">=2.18.0,<3.0.0" [package.extras] async-rest = ["google-auth[aiohttp] (>=2.35.0,<3.0.dev0)"] -grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev)", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0)"] +grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev) ; python_version >= \"3.11\"", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0) ; python_version >= \"3.11\""] grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] @@ -832,6 +878,7 @@ version = "2.39.0" description = "Google Authentication Library" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "google_auth-2.39.0-py2.py3-none-any.whl", hash = "sha256:0150b6711e97fb9f52fe599f55648950cc4540015565d8fbb31be2ad6e1548a2"}, {file = "google_auth-2.39.0.tar.gz", hash = "sha256:73222d43cdc35a3aeacbfdcaf73142a97839f10de930550d89ebfe1d0a00cde7"}, @@ -845,11 +892,11 @@ rsa = ">=3.1.4,<5" [package.extras] aiohttp = ["aiohttp (>=3.6.2,<4.0.0)", "requests (>=2.20.0,<3.0.0)"] enterprise-cert = ["cryptography", "pyopenssl"] -pyjwt = ["cryptography (<39.0.0)", "cryptography (>=38.0.3)", "pyjwt (>=2.0)"] -pyopenssl = ["cryptography (<39.0.0)", "cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] +pyjwt = ["cryptography (<39.0.0) ; python_version < \"3.8\"", "cryptography (>=38.0.3)", "pyjwt (>=2.0)"] +pyopenssl = ["cryptography (<39.0.0) ; python_version < \"3.8\"", "cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] reauth = ["pyu2f (>=0.1.5)"] requests = ["requests (>=2.20.0,<3.0.0)"] -testing = ["aiohttp (<3.10.0)", "aiohttp (>=3.6.2,<4.0.0)", "aioresponses", "cryptography (<39.0.0)", "cryptography (>=38.0.3)", "flask", "freezegun", "grpcio", "mock", "oauth2client", "packaging", "pyjwt (>=2.0)", "pyopenssl (<24.3.0)", "pyopenssl (>=20.0.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-localserver", "pyu2f (>=0.1.5)", "requests (>=2.20.0,<3.0.0)", "responses", "urllib3"] +testing = ["aiohttp (<3.10.0)", "aiohttp (>=3.6.2,<4.0.0)", "aioresponses", "cryptography (<39.0.0) ; python_version < \"3.8\"", "cryptography (>=38.0.3)", "flask", "freezegun", "grpcio", "mock", "oauth2client", "packaging", "pyjwt (>=2.0)", "pyopenssl (<24.3.0)", "pyopenssl (>=20.0.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-localserver", "pyu2f (>=0.1.5)", "requests (>=2.20.0,<3.0.0)", "responses", "urllib3"] urllib3 = ["packaging", "urllib3"] [[package]] @@ -858,6 +905,7 @@ version = "1.70.0" description = "Common protobufs used in Google APIs" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "googleapis_common_protos-1.70.0-py3-none-any.whl", hash = "sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8"}, {file = "googleapis_common_protos-1.70.0.tar.gz", hash = "sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257"}, @@ -875,6 +923,8 @@ version = "3.2.1" description = "Lightweight in-process concurrent programming" optional = false python-versions = ">=3.9" +groups = ["main"] +markers = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")" files = [ {file = "greenlet-3.2.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:777c1281aa7c786738683e302db0f55eb4b0077c20f1dc53db8852ffaea0a6b0"}, {file = "greenlet-3.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3059c6f286b53ea4711745146ffe5a5c5ff801f62f6c56949446e0f6461f8157"}, @@ -943,6 +993,7 @@ version = "0.23.1" description = "The official Python library for the groq API" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "groq-0.23.1-py3-none-any.whl", hash = "sha256:05fa38c3d0ad03c19c6185f98f6a73901c2a463e844fd067b79f7b05c8346946"}, {file = "groq-0.23.1.tar.gz", hash = "sha256:952e34895f9bfb78ab479e495d77b32180262e5c42f531ce3a1722d6e5a04dfb"}, @@ -962,6 +1013,7 @@ version = "1.71.0" description = "HTTP/2-based RPC framework" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "grpcio-1.71.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:c200cb6f2393468142eb50ab19613229dcc7829b5ccee8b658a36005f6669fdd"}, {file = "grpcio-1.71.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:b2266862c5ad664a380fbbcdbdb8289d71464c42a8c29053820ee78ba0119e5d"}, @@ -1025,6 +1077,7 @@ version = "1.62.3" description = "Status proto mapping for gRPC" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "grpcio-status-1.62.3.tar.gz", hash = "sha256:289bdd7b2459794a12cf95dc0cb727bd4a1742c37bd823f760236c937e53a485"}, {file = "grpcio_status-1.62.3-py3-none-any.whl", hash = "sha256:f9049b762ba8de6b1086789d8315846e094edac2c50beaf462338b301a8fd4b8"}, @@ -1041,6 +1094,7 @@ version = "0.16.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, @@ -1052,6 +1106,7 @@ version = "1.0.9" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, @@ -1073,6 +1128,7 @@ version = "0.6.4" description = "A collection of framework independent HTTP protocol utils." optional = false python-versions = ">=3.8.0" +groups = ["main"] files = [ {file = "httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0"}, {file = "httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da"}, @@ -1128,6 +1184,7 @@ version = "0.27.2" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, @@ -1141,7 +1198,7 @@ idna = "*" sniffio = "*" [package.extras] -brotli = ["brotli", "brotlicffi"] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] @@ -1153,6 +1210,7 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -1167,6 +1225,7 @@ version = "2.1.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, @@ -1178,6 +1237,7 @@ version = "5.13.2" description = "A Python utility / library to sort Python imports." optional = false python-versions = ">=3.8.0" +groups = ["dev"] files = [ {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, @@ -1192,6 +1252,7 @@ version = "0.9.0" description = "Fast iterable JSON parser." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "jiter-0.9.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:816ec9b60fdfd1fec87da1d7ed46c66c44ffec37ab2ef7de5b147b2fce3fd5ad"}, {file = "jiter-0.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9b1d3086f8a3ee0194ecf2008cf81286a5c3e540d977fa038ff23576c023c0ea"}, @@ -1277,6 +1338,7 @@ version = "1.33" description = "Apply JSON-Patches (RFC 6902)" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" +groups = ["main"] files = [ {file = "jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade"}, {file = "jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c"}, @@ -1291,6 +1353,7 @@ version = "3.0.0" description = "Identify specific nodes in a JSON document (RFC 6901)" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"}, {file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"}, @@ -1302,6 +1365,7 @@ version = "1.4.8" description = "A fast implementation of the Cassowary constraint solver" optional = false python-versions = ">=3.10" +groups = ["main"] files = [ {file = "kiwisolver-1.4.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88c6f252f6816a73b1f8c904f7bbe02fd67c09a69f7cb8a0eecdbf5ce78e63db"}, {file = "kiwisolver-1.4.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72941acb7b67138f35b879bbe85be0f6c6a70cab78fe3ef6db9c024d9223e5b"}, @@ -1391,6 +1455,7 @@ version = "0.3.0" description = "Building applications with LLMs through composability" optional = false python-versions = "<4.0,>=3.9" +groups = ["main"] files = [ {file = "langchain-0.3.0-py3-none-any.whl", hash = "sha256:59a75a6a1eb7bfd2a6bf0c7a5816409a8fdc9046187b07af287b23b9899617af"}, {file = "langchain-0.3.0.tar.gz", hash = "sha256:a7c23892440bd1f5b9e029ff0dd709dd881ae927c4c0a3210ac64dba9bbf3f7f"}, @@ -1417,6 +1482,7 @@ version = "0.3.5" description = "An integration package connecting AnthropicMessages and LangChain" optional = false python-versions = "<4.0,>=3.9" +groups = ["main"] files = [ {file = "langchain_anthropic-0.3.5-py3-none-any.whl", hash = "sha256:bad34b02d7b4bdca9a9471bc391b01269fd8dc4600b83ca2a3e76925b7c27fe6"}, {file = "langchain_anthropic-0.3.5.tar.gz", hash = "sha256:2aa1673511056061680492871f386d68a8b62947e0eb1f15303ef10db16c8357"}, @@ -1434,6 +1500,7 @@ version = "0.3.56" description = "Building applications with LLMs through composability" optional = false python-versions = "<4.0,>=3.9" +groups = ["main"] files = [ {file = "langchain_core-0.3.56-py3-none-any.whl", hash = "sha256:a20c6aca0fa0da265d96d3b14a5a01828ac5d2d9d27516434873d76f2d4839ed"}, {file = "langchain_core-0.3.56.tar.gz", hash = "sha256:de896585bc56e12652327dcd195227c3739a07e86e587c91a07101e0df11dffe"}, @@ -1457,6 +1524,7 @@ version = "0.1.3" description = "An integration package connecting DeepSeek and LangChain" optional = false python-versions = "<4.0,>=3.9" +groups = ["main"] files = [ {file = "langchain_deepseek-0.1.3-py3-none-any.whl", hash = "sha256:8588e826371b417fca65c02f4273b4061eb9815a7bfcd5eb05acaa40d603aa89"}, {file = "langchain_deepseek-0.1.3.tar.gz", hash = "sha256:89dd6aa120fb50dcfcd3d593626d34c1c40deefe4510710d0807fcc19481adf5"}, @@ -1472,6 +1540,7 @@ version = "2.1.3" description = "An integration package connecting Google's genai package and LangChain" optional = false python-versions = "<4.0,>=3.9" +groups = ["main"] files = [ {file = "langchain_google_genai-2.1.3-py3-none-any.whl", hash = "sha256:adf222931ac7af543f4013751a9b7dbd9ed637fb4eb3e4e0cd7e1d5d7e066d36"}, {file = "langchain_google_genai-2.1.3.tar.gz", hash = "sha256:0d4e2abf01a7594a9420d3569cf2cd4239a01cc24c6698d3c2c92a072b9b7b4a"}, @@ -1489,6 +1558,7 @@ version = "0.2.3" description = "An integration package connecting Groq and LangChain" optional = false python-versions = "<4.0,>=3.9" +groups = ["main"] files = [ {file = "langchain_groq-0.2.3-py3-none-any.whl", hash = "sha256:3572c812acc1478ab0670c48eb9a135c95f47631190da750e48408267462a12d"}, {file = "langchain_groq-0.2.3.tar.gz", hash = "sha256:f94810fe734c9402b36273ddc3509eaa67f12a7d06b666c6ca472ab0bfdf37b7"}, @@ -1504,6 +1574,7 @@ version = "0.2.3" description = "An integration package connecting Ollama and LangChain" optional = false python-versions = "<4.0,>=3.9" +groups = ["main"] files = [ {file = "langchain_ollama-0.2.3-py3-none-any.whl", hash = "sha256:c47700ca68b013358b1e954493ecafb3bd10fa2cda71a9f15ba7897587a9aab2"}, {file = "langchain_ollama-0.2.3.tar.gz", hash = "sha256:d13fe8735176b652ca6e6656d7902c1265e8c0601097569f7c95433f3d034b38"}, @@ -1519,6 +1590,7 @@ version = "0.3.14" description = "An integration package connecting OpenAI and LangChain" optional = false python-versions = "<4.0,>=3.9" +groups = ["main"] files = [ {file = "langchain_openai-0.3.14-py3-none-any.whl", hash = "sha256:b8e648d2d7678a5540818199d141ff727c6f1514294b3e1e999a95357c9d66a0"}, {file = "langchain_openai-0.3.14.tar.gz", hash = "sha256:0662db78620c2e5c3ccfc1c36dc959c0ddc80e6bdf7ef81632cbf4b2cc9b9461"}, @@ -1535,6 +1607,7 @@ version = "0.3.8" description = "LangChain text splitting utilities" optional = false python-versions = "<4.0,>=3.9" +groups = ["main"] files = [ {file = "langchain_text_splitters-0.3.8-py3-none-any.whl", hash = "sha256:e75cc0f4ae58dcf07d9f18776400cf8ade27fadd4ff6d264df6278bb302f6f02"}, {file = "langchain_text_splitters-0.3.8.tar.gz", hash = "sha256:116d4b9f2a22dda357d0b79e30acf005c5518177971c66a9f1ab0edfdb0f912e"}, @@ -1549,6 +1622,7 @@ version = "0.2.56" description = "Building stateful, multi-actor applications with LLMs" optional = false python-versions = "<4.0,>=3.9.0" +groups = ["main"] files = [ {file = "langgraph-0.2.56-py3-none-any.whl", hash = "sha256:ad8a4b772e34dc0137e890bb6ced596a39a1e684af66250c1e7c8150dbe90e9c"}, {file = "langgraph-0.2.56.tar.gz", hash = "sha256:af10b1ffd10d52fd4072a73f154b8c2513c0b22e5bd5d20f4567dfeecab98d1e"}, @@ -1565,6 +1639,7 @@ version = "2.0.25" description = "Library with base interfaces for LangGraph checkpoint savers." optional = false python-versions = "<4.0.0,>=3.9.0" +groups = ["main"] files = [ {file = "langgraph_checkpoint-2.0.25-py3-none-any.whl", hash = "sha256:23416a0f5bc9dd712ac10918fc13e8c9c4530c419d2985a441df71a38fc81602"}, {file = "langgraph_checkpoint-2.0.25.tar.gz", hash = "sha256:77a63cab7b5f84dec1d49db561326ec28bdd48bcefb7fe4ac372069d2609287b"}, @@ -1580,6 +1655,7 @@ version = "0.1.63" description = "SDK for interacting with LangGraph API" optional = false python-versions = "<4.0.0,>=3.9.0" +groups = ["main"] files = [ {file = "langgraph_sdk-0.1.63-py3-none-any.whl", hash = "sha256:6fb78a7fc6a30eea43bd0d6401dbc9e3263d0d4c03f63c04035980da7e586b05"}, {file = "langgraph_sdk-0.1.63.tar.gz", hash = "sha256:62bf2cc31e5aa6c5b9011ee1702bcf1e36e67e142a60bd97af2611162fb58e18"}, @@ -1595,6 +1671,7 @@ version = "0.1.147" description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform." optional = false python-versions = "<4.0,>=3.8.1" +groups = ["main"] files = [ {file = "langsmith-0.1.147-py3-none-any.whl", hash = "sha256:7166fc23b965ccf839d64945a78e9f1157757add228b086141eb03a60d699a15"}, {file = "langsmith-0.1.147.tar.gz", hash = "sha256:2e933220318a4e73034657103b3b1a3a6109cc5db3566a7e8e03be8d6d7def7a"}, @@ -1619,6 +1696,7 @@ version = "1.3.10" description = "A super-fast templating language that borrows the best ideas from the existing templating languages." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59"}, {file = "mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28"}, @@ -1638,6 +1716,7 @@ version = "3.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, @@ -1662,6 +1741,7 @@ version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, @@ -1732,6 +1812,7 @@ version = "3.10.1" description = "Python plotting package" optional = false python-versions = ">=3.10" +groups = ["main"] files = [ {file = "matplotlib-3.10.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ff2ae14910be903f4a24afdbb6d7d3a6c44da210fc7d42790b87aeac92238a16"}, {file = "matplotlib-3.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0721a3fd3d5756ed593220a8b86808a36c5031fce489adb5b31ee6dbb47dd5b2"}, @@ -1789,6 +1870,7 @@ version = "0.7.0" description = "McCabe checker, plugin for flake8" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, @@ -1800,17 +1882,88 @@ version = "0.1.2" description = "Markdown URL utilities" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] +[[package]] +name = "msgpack" +version = "1.1.1" +description = "MessagePack serializer" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "msgpack-1.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:353b6fc0c36fde68b661a12949d7d49f8f51ff5fa019c1e47c87c4ff34b080ed"}, + {file = "msgpack-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:79c408fcf76a958491b4e3b103d1c417044544b68e96d06432a189b43d1215c8"}, + {file = "msgpack-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78426096939c2c7482bf31ef15ca219a9e24460289c00dd0b94411040bb73ad2"}, + {file = "msgpack-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b17ba27727a36cb73aabacaa44b13090feb88a01d012c0f4be70c00f75048b4"}, + {file = "msgpack-1.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a17ac1ea6ec3c7687d70201cfda3b1e8061466f28f686c24f627cae4ea8efd0"}, + {file = "msgpack-1.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:88d1e966c9235c1d4e2afac21ca83933ba59537e2e2727a999bf3f515ca2af26"}, + {file = "msgpack-1.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f6d58656842e1b2ddbe07f43f56b10a60f2ba5826164910968f5933e5178af75"}, + {file = "msgpack-1.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:96decdfc4adcbc087f5ea7ebdcfd3dee9a13358cae6e81d54be962efc38f6338"}, + {file = "msgpack-1.1.1-cp310-cp310-win32.whl", hash = "sha256:6640fd979ca9a212e4bcdf6eb74051ade2c690b862b679bfcb60ae46e6dc4bfd"}, + {file = "msgpack-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:8b65b53204fe1bd037c40c4148d00ef918eb2108d24c9aaa20bc31f9810ce0a8"}, + {file = "msgpack-1.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:71ef05c1726884e44f8b1d1773604ab5d4d17729d8491403a705e649116c9558"}, + {file = "msgpack-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:36043272c6aede309d29d56851f8841ba907a1a3d04435e43e8a19928e243c1d"}, + {file = "msgpack-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a32747b1b39c3ac27d0670122b57e6e57f28eefb725e0b625618d1b59bf9d1e0"}, + {file = "msgpack-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a8b10fdb84a43e50d38057b06901ec9da52baac6983d3f709d8507f3889d43f"}, + {file = "msgpack-1.1.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba0c325c3f485dc54ec298d8b024e134acf07c10d494ffa24373bea729acf704"}, + {file = "msgpack-1.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:88daaf7d146e48ec71212ce21109b66e06a98e5e44dca47d853cbfe171d6c8d2"}, + {file = "msgpack-1.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8b55ea20dc59b181d3f47103f113e6f28a5e1c89fd5b67b9140edb442ab67f2"}, + {file = "msgpack-1.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4a28e8072ae9779f20427af07f53bbb8b4aa81151054e882aee333b158da8752"}, + {file = "msgpack-1.1.1-cp311-cp311-win32.whl", hash = "sha256:7da8831f9a0fdb526621ba09a281fadc58ea12701bc709e7b8cbc362feabc295"}, + {file = "msgpack-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:5fd1b58e1431008a57247d6e7cc4faa41c3607e8e7d4aaf81f7c29ea013cb458"}, + {file = "msgpack-1.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ae497b11f4c21558d95de9f64fff7053544f4d1a17731c866143ed6bb4591238"}, + {file = "msgpack-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:33be9ab121df9b6b461ff91baac6f2731f83d9b27ed948c5b9d1978ae28bf157"}, + {file = "msgpack-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f64ae8fe7ffba251fecb8408540c34ee9df1c26674c50c4544d72dbf792e5ce"}, + {file = "msgpack-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a494554874691720ba5891c9b0b39474ba43ffb1aaf32a5dac874effb1619e1a"}, + {file = "msgpack-1.1.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb643284ab0ed26f6957d969fe0dd8bb17beb567beb8998140b5e38a90974f6c"}, + {file = "msgpack-1.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d275a9e3c81b1093c060c3837e580c37f47c51eca031f7b5fb76f7b8470f5f9b"}, + {file = "msgpack-1.1.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4fd6b577e4541676e0cc9ddc1709d25014d3ad9a66caa19962c4f5de30fc09ef"}, + {file = "msgpack-1.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb29aaa613c0a1c40d1af111abf025f1732cab333f96f285d6a93b934738a68a"}, + {file = "msgpack-1.1.1-cp312-cp312-win32.whl", hash = "sha256:870b9a626280c86cff9c576ec0d9cbcc54a1e5ebda9cd26dab12baf41fee218c"}, + {file = "msgpack-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:5692095123007180dca3e788bb4c399cc26626da51629a31d40207cb262e67f4"}, + {file = "msgpack-1.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3765afa6bd4832fc11c3749be4ba4b69a0e8d7b728f78e68120a157a4c5d41f0"}, + {file = "msgpack-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8ddb2bcfd1a8b9e431c8d6f4f7db0773084e107730ecf3472f1dfe9ad583f3d9"}, + {file = "msgpack-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:196a736f0526a03653d829d7d4c5500a97eea3648aebfd4b6743875f28aa2af8"}, + {file = "msgpack-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d592d06e3cc2f537ceeeb23d38799c6ad83255289bb84c2e5792e5a8dea268a"}, + {file = "msgpack-1.1.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4df2311b0ce24f06ba253fda361f938dfecd7b961576f9be3f3fbd60e87130ac"}, + {file = "msgpack-1.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e4141c5a32b5e37905b5940aacbc59739f036930367d7acce7a64e4dec1f5e0b"}, + {file = "msgpack-1.1.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b1ce7f41670c5a69e1389420436f41385b1aa2504c3b0c30620764b15dded2e7"}, + {file = "msgpack-1.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4147151acabb9caed4e474c3344181e91ff7a388b888f1e19ea04f7e73dc7ad5"}, + {file = "msgpack-1.1.1-cp313-cp313-win32.whl", hash = "sha256:500e85823a27d6d9bba1d057c871b4210c1dd6fb01fbb764e37e4e8847376323"}, + {file = "msgpack-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:6d489fba546295983abd142812bda76b57e33d0b9f5d5b71c09a583285506f69"}, + {file = "msgpack-1.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bba1be28247e68994355e028dcd668316db30c1f758d3241a7b903ac78dcd285"}, + {file = "msgpack-1.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8f93dcddb243159c9e4109c9750ba5b335ab8d48d9522c5308cd05d7e3ce600"}, + {file = "msgpack-1.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fbbc0b906a24038c9958a1ba7ae0918ad35b06cb449d398b76a7d08470b0ed9"}, + {file = "msgpack-1.1.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:61e35a55a546a1690d9d09effaa436c25ae6130573b6ee9829c37ef0f18d5e78"}, + {file = "msgpack-1.1.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:1abfc6e949b352dadf4bce0eb78023212ec5ac42f6abfd469ce91d783c149c2a"}, + {file = "msgpack-1.1.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:996f2609ddf0142daba4cefd767d6db26958aac8439ee41db9cc0db9f4c4c3a6"}, + {file = "msgpack-1.1.1-cp38-cp38-win32.whl", hash = "sha256:4d3237b224b930d58e9d83c81c0dba7aacc20fcc2f89c1e5423aa0529a4cd142"}, + {file = "msgpack-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:da8f41e602574ece93dbbda1fab24650d6bf2a24089f9e9dbb4f5730ec1e58ad"}, + {file = "msgpack-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5be6b6bc52fad84d010cb45433720327ce886009d862f46b26d4d154001994b"}, + {file = "msgpack-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3a89cd8c087ea67e64844287ea52888239cbd2940884eafd2dcd25754fb72232"}, + {file = "msgpack-1.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d75f3807a9900a7d575d8d6674a3a47e9f227e8716256f35bc6f03fc597ffbf"}, + {file = "msgpack-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d182dac0221eb8faef2e6f44701812b467c02674a322c739355c39e94730cdbf"}, + {file = "msgpack-1.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b13fe0fb4aac1aa5320cd693b297fe6fdef0e7bea5518cbc2dd5299f873ae90"}, + {file = "msgpack-1.1.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:435807eeb1bc791ceb3247d13c79868deb22184e1fc4224808750f0d7d1affc1"}, + {file = "msgpack-1.1.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4835d17af722609a45e16037bb1d4d78b7bdf19d6c0128116d178956618c4e88"}, + {file = "msgpack-1.1.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a8ef6e342c137888ebbfb233e02b8fbd689bb5b5fcc59b34711ac47ebd504478"}, + {file = "msgpack-1.1.1-cp39-cp39-win32.whl", hash = "sha256:61abccf9de335d9efd149e2fff97ed5974f2481b3353772e8e2dd3402ba2bd57"}, + {file = "msgpack-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:40eae974c873b2992fd36424a5d9407f93e97656d999f43fca9d29f820899084"}, + {file = "msgpack-1.1.1.tar.gz", hash = "sha256:77b79ce34a2bdab2594f490c8e80dd62a02d650b91a75159a63ec413b8d104cd"}, +] + [[package]] name = "multidict" version = "6.4.3" description = "multidict implementation" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "multidict-6.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32a998bd8a64ca48616eac5a8c1cc4fa38fb244a3facf2eeb14abe186e0f6cc5"}, {file = "multidict-6.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a54ec568f1fc7f3c313c2f3b16e5db346bf3660e1309746e7fccbbfded856188"}, @@ -1924,6 +2077,7 @@ version = "1.1.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, @@ -1935,6 +2089,7 @@ version = "1.26.4" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, @@ -1980,6 +2135,7 @@ version = "0.4.8" description = "The official Python client for Ollama." optional = false python-versions = "<4.0,>=3.8" +groups = ["main"] files = [ {file = "ollama-0.4.8-py3-none-any.whl", hash = "sha256:04312af2c5e72449aaebac4a2776f52ef010877c554103419d3f36066fe8af4c"}, {file = "ollama-0.4.8.tar.gz", hash = "sha256:1121439d49b96fa8339842965d0616eba5deb9f8c790786cdf4c0b3df4833802"}, @@ -1995,6 +2151,7 @@ version = "1.76.2" description = "The official Python library for the openai API" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "openai-1.76.2-py3-none-any.whl", hash = "sha256:9c1d9ad59e6e3bea7205eedc9ca66eeebae18d47b527e505a2b0d2fb1538e26e"}, {file = "openai-1.76.2.tar.gz", hash = "sha256:f430c8b848775907405c6eff54621254c96f6444c593c097e0cc3a9f8fdda96f"}, @@ -2021,6 +2178,7 @@ version = "3.10.17" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "orjson-3.10.17-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bc399cf138a0201d0bf2399b44195d33a0a5aee149dab114340da0d766c88b95"}, {file = "orjson-3.10.17-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59225b27b72e0e1626d869f7b987da6c74f9b6026cf9a87c1cdaf74ca9f7b8c0"}, @@ -2102,6 +2260,7 @@ version = "1.9.1" description = "Fast, correct Python msgpack library supporting dataclasses, datetimes, and numpy" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "ormsgpack-1.9.1-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:f1f804fd9c0fd84213a6022c34172f82323b34afa7052a4af18797582cf56365"}, {file = "ormsgpack-1.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eab5cec99c46276b37071d570aab98603f3d0309b3818da3247eb64bb95e5cfc"}, @@ -2152,6 +2311,7 @@ version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, @@ -2163,6 +2323,7 @@ version = "2.2.3" description = "Powerful data structures for data analysis, time series, and statistics" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5"}, {file = "pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348"}, @@ -2248,6 +2409,7 @@ version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, @@ -2259,6 +2421,7 @@ version = "11.2.1" description = "Python Imaging Library (Fork)" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "pillow-11.2.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:d57a75d53922fc20c165016a20d9c44f73305e67c351bbc60d1adaf662e74047"}, {file = "pillow-11.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:127bf6ac4a5b58b3d32fc8289656f77f80567d65660bc46f72c0d77e6600cc95"}, @@ -2349,7 +2512,7 @@ fpx = ["olefile"] mic = ["olefile"] test-arrow = ["pyarrow"] tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "trove-classifiers (>=2024.10.12)"] -typing = ["typing-extensions"] +typing = ["typing-extensions ; python_version < \"3.10\""] xmp = ["defusedxml"] [[package]] @@ -2358,6 +2521,7 @@ version = "4.3.7" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94"}, {file = "platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351"}, @@ -2374,6 +2538,7 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -2389,6 +2554,7 @@ version = "3.0.51" description = "Library for building powerful interactive command lines in Python" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07"}, {file = "prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed"}, @@ -2403,6 +2569,7 @@ version = "0.3.1" description = "Accelerated property cache" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "propcache-0.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f27785888d2fdd918bc36de8b8739f2d6c791399552333721b58193f68ea3e98"}, {file = "propcache-0.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4e89cde74154c7b5957f87a355bb9c8ec929c167b59c83d90654ea36aeb6180"}, @@ -2510,6 +2677,7 @@ version = "1.26.1" description = "Beautiful, Pythonic protocol buffers" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66"}, {file = "proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012"}, @@ -2527,6 +2695,7 @@ version = "6.30.2" description = "" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "protobuf-6.30.2-cp310-abi3-win32.whl", hash = "sha256:b12ef7df7b9329886e66404bef5e9ce6a26b54069d7f7436a0853ccdeb91c103"}, {file = "protobuf-6.30.2-cp310-abi3-win_amd64.whl", hash = "sha256:7653c99774f73fe6b9301b87da52af0e69783a2e371e8b599b3e9cb4da4b12b9"}, @@ -2545,6 +2714,7 @@ version = "0.6.1" description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"}, {file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"}, @@ -2556,6 +2726,7 @@ version = "0.4.2" description = "A collection of ASN.1-based protocols modules" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a"}, {file = "pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6"}, @@ -2570,6 +2741,7 @@ version = "2.11.1" description = "Python style guide checker" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, @@ -2581,6 +2753,7 @@ version = "2.11.4" description = "Data validation using Python type hints" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "pydantic-2.11.4-py3-none-any.whl", hash = "sha256:d9615eaa9ac5a063471da949c8fc16376a84afb5024688b3ff885693506764eb"}, {file = "pydantic-2.11.4.tar.gz", hash = "sha256:32738d19d63a226a52eed76645a98ee07c1f410ee41d93b4afbfa85ed8111c2d"}, @@ -2594,7 +2767,7 @@ typing-inspection = ">=0.4.0" [package.extras] email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] [[package]] name = "pydantic-core" @@ -2602,6 +2775,7 @@ version = "2.33.2" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, @@ -2713,6 +2887,7 @@ version = "3.1.0" description = "passive checker of Python programs" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pyflakes-3.1.0-py2.py3-none-any.whl", hash = "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774"}, {file = "pyflakes-3.1.0.tar.gz", hash = "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"}, @@ -2724,6 +2899,7 @@ version = "2.19.1" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, @@ -2738,6 +2914,7 @@ version = "3.2.3" description = "pyparsing module - Classes and methods to define and execute parsing grammars" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf"}, {file = "pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be"}, @@ -2752,6 +2929,7 @@ version = "7.4.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, @@ -2772,6 +2950,7 @@ version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -2786,6 +2965,7 @@ version = "1.0.0" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"}, {file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"}, @@ -2800,6 +2980,7 @@ version = "2025.2" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, @@ -2811,6 +2992,7 @@ version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -2873,6 +3055,7 @@ version = "2.1.0" description = "Python library to build pretty command line user prompts ⭐️" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "questionary-2.1.0-py3-none-any.whl", hash = "sha256:44174d237b68bc828e4878c763a9ad6790ee61990e0ae72927694ead57bab8ec"}, {file = "questionary-2.1.0.tar.gz", hash = "sha256:6302cdd645b19667d8f6e6634774e9538bfcd1aad9be287e743d96cacaf95587"}, @@ -2887,6 +3070,7 @@ version = "2024.11.6" description = "Alternative regular expression module, to replace re." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"}, {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0"}, @@ -2990,6 +3174,7 @@ version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, @@ -3011,6 +3196,7 @@ version = "1.0.0" description = "A utility belt for advanced users of python-requests" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] files = [ {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, @@ -3025,6 +3211,7 @@ version = "13.9.4" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.8.0" +groups = ["main"] files = [ {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, @@ -3043,6 +3230,7 @@ version = "0.14.4" description = "Rich toolkit for building command-line applications" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "rich_toolkit-0.14.4-py3-none-any.whl", hash = "sha256:cc71ebee83eaa122d8e42882408bc5a4bf0240bbf1e368811ee56d249b3d742a"}, {file = "rich_toolkit-0.14.4.tar.gz", hash = "sha256:db256cf45165cae381c9bbf3b48a0fd4d99a07c80155cc655c80212a62e28fe1"}, @@ -3059,6 +3247,7 @@ version = "4.9.1" description = "Pure-Python RSA implementation" optional = false python-versions = "<4,>=3.6" +groups = ["main"] files = [ {file = "rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762"}, {file = "rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75"}, @@ -3073,6 +3262,7 @@ version = "1.5.4" description = "Tool to Detect Surrounding Shell" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, @@ -3084,6 +3274,7 @@ version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -3095,6 +3286,7 @@ version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, @@ -3106,6 +3298,7 @@ version = "2.0.40" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "SQLAlchemy-2.0.40-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ae9597cab738e7cc823f04a704fb754a9249f0b6695a6aeb63b74055cd417a96"}, {file = "SQLAlchemy-2.0.40-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37a5c21ab099a83d669ebb251fddf8f5cee4d75ea40a5a1653d9c43d60e20867"}, @@ -3195,12 +3388,25 @@ postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] pymysql = ["pymysql"] sqlcipher = ["sqlcipher3_binary"] +[[package]] +name = "sseclient-py" +version = "1.8.0" +description = "SSE client for Python" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "sseclient-py-1.8.0.tar.gz", hash = "sha256:c547c5c1a7633230a38dc599a21a2dc638f9b5c297286b48b46b935c71fac3e8"}, + {file = "sseclient_py-1.8.0-py2.py3-none-any.whl", hash = "sha256:4ecca6dc0b9f963f8384e9d7fd529bf93dd7d708144c4fb5da0e0a1a926fee83"}, +] + [[package]] name = "starlette" version = "0.27.0" description = "The little ASGI library that shines." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "starlette-0.27.0-py3-none-any.whl", hash = "sha256:918416370e846586541235ccd38a474c08b80443ed31c578a418e2209b3eef91"}, {file = "starlette-0.27.0.tar.gz", hash = "sha256:6a6b0d042acb8d469a01eba54e9cda6cbd24ac602c4cd016723117d6a7e73b75"}, @@ -3218,6 +3424,7 @@ version = "0.9.0" description = "Pretty-print tabular data" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"}, {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"}, @@ -3232,6 +3439,7 @@ version = "8.5.0" description = "Retry code until it succeeds" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "tenacity-8.5.0-py3-none-any.whl", hash = "sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687"}, {file = "tenacity-8.5.0.tar.gz", hash = "sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78"}, @@ -3247,6 +3455,7 @@ version = "0.9.0" description = "tiktoken is a fast BPE tokeniser for use with OpenAI's models" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "tiktoken-0.9.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:586c16358138b96ea804c034b8acf3f5d3f0258bd2bc3b0227af4af5d622e382"}, {file = "tiktoken-0.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d9c59ccc528c6c5dd51820b3474402f69d9a9e1d656226848ad68a8d5b2e5108"}, @@ -3294,6 +3503,7 @@ version = "4.67.1" description = "Fast, Extensible Progress Meter" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, @@ -3315,6 +3525,7 @@ version = "0.15.3" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "typer-0.15.3-py3-none-any.whl", hash = "sha256:c86a65ad77ca531f03de08d1b9cb67cd09ad02ddddf4b34745b5008f43b239bd"}, {file = "typer-0.15.3.tar.gz", hash = "sha256:818873625d0569653438316567861899f7e9972f2e6e0c16dab608345ced713c"}, @@ -3332,6 +3543,7 @@ version = "4.13.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, @@ -3343,6 +3555,7 @@ version = "0.4.0" description = "Runtime typing introspection tools" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f"}, {file = "typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122"}, @@ -3357,6 +3570,7 @@ version = "2025.2" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" +groups = ["main"] files = [ {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, @@ -3368,13 +3582,14 @@ version = "2.4.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813"}, {file = "urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -3385,6 +3600,7 @@ version = "0.34.2" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403"}, {file = "uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328"}, @@ -3397,12 +3613,12 @@ h11 = ">=0.8" httptools = {version = ">=0.6.3", optional = true, markers = "extra == \"standard\""} python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} -uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "(sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_python_implementation != \"PyPy\" and extra == \"standard\""} +uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\" and extra == \"standard\""} watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""} [package.extras] -standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] +standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] [[package]] name = "uvloop" @@ -3410,6 +3626,8 @@ version = "0.21.0" description = "Fast implementation of asyncio event loop on top of libuv" optional = false python-versions = ">=3.8.0" +groups = ["main"] +markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"" files = [ {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f"}, {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d"}, @@ -3461,6 +3679,7 @@ version = "1.0.5" description = "Simple, modern and high performance file watching and code reload in python." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "watchfiles-1.0.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:5c40fe7dd9e5f81e0847b1ea64e1f5dd79dd61afbedb57759df06767ac719b40"}, {file = "watchfiles-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8c0db396e6003d99bb2d7232c957b5f0b5634bbd1b24e381a5afcc880f7373fb"}, @@ -3544,6 +3763,7 @@ version = "0.2.13" description = "Measures the displayed width of unicode strings in a terminal" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, @@ -3555,6 +3775,7 @@ version = "15.0.1" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b"}, {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205"}, @@ -3633,6 +3854,7 @@ version = "1.20.0" description = "Yet another URL library" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "yarl-1.20.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f1f6670b9ae3daedb325fa55fbe31c22c8228f6e0b513772c2e1c623caa6ab22"}, {file = "yarl-1.20.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85a231fa250dfa3308f3c7896cc007a47bc76e9e8e8595c20b7426cac4884c62"}, @@ -3746,6 +3968,6 @@ multidict = ">=4.0" propcache = ">=0.2.1" [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "^3.11" -content-hash = "4b63c0cbfd37b7262e6b1364ae4e7ce6c8b410933f3f8e62f12af5ed9646203e" +content-hash = "680ad21758c391683e2eab88c4c6733c3c9b41942066b999e18769d313e64dbd" diff --git a/pyproject.toml b/pyproject.toml index 1483d487b..9f321b6f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ pydantic = "^2.4.2" httpx = "^0.27.0" sqlalchemy = "^2.0.22" alembic = "^1.12.0" +alpaca-py = "^0.40.2" [tool.poetry.group.dev.dependencies] pytest = "^7.4.0" diff --git a/src/trader.py b/src/trader.py new file mode 100644 index 000000000..a097a7526 --- /dev/null +++ b/src/trader.py @@ -0,0 +1,267 @@ +"""Live trading CLI for the AI hedge fund.""" + +import sys +import argparse +from dotenv import load_dotenv +import questionary +from colorama import Fore, Style, init + +from src.trading.trader import create_trader +from src.utils.analysts import ANALYST_ORDER +from src.llm.models import LLM_ORDER, OLLAMA_LLM_ORDER, get_model_info, ModelProvider +from src.utils.ollama import ensure_ollama_and_model + +# Load environment variables +load_dotenv() +init(autoreset=True) + + +def main(): + """Main CLI entry point for live trading.""" + parser = argparse.ArgumentParser(description="Run live trading with AI hedge fund") + parser.add_argument("--tickers", type=str, help="Comma-separated list of stock ticker symbols") + parser.add_argument("--dry-run", action="store_true", help="Show decisions but don't execute trades") + parser.add_argument("--ignore-market-hours", action="store_true", help="Run even when market is closed (useful with --dry-run)") + parser.add_argument("--continuous", action="store_true", help="Run continuous trading") + parser.add_argument("--interval", type=int, default=60, help="Trading interval in minutes (default: 60)") + parser.add_argument("--available-capital", type=float, help="Override available capital (uses broker cash if not specified)") + parser.add_argument("--margin-requirement", type=float, help="Margin requirement ratio for short positions (uses broker default if not specified)") + parser.add_argument("--show-reasoning", action="store_true", help="Show reasoning from each agent") + parser.add_argument("--ollama", action="store_true", help="Use Ollama for local LLM inference") + parser.add_argument("--analysts", type=str, help="Comma-separated list of analysts to use") + parser.add_argument("--analysts-all", action="store_true", help="Use all available analysts") + parser.add_argument("--list-analysts", action="store_true", help="List all available analysts and exit") + parser.add_argument("--model", type=str, help="LLM model name to use") + parser.add_argument("--list-models", action="store_true", help="List all available models and exit") + + args = parser.parse_args() + + # Handle list options + if args.list_analysts: + print(f"\n{Fore.CYAN}Available Analysts:{Style.RESET_ALL}") + for display, value in ANALYST_ORDER: + print(f" {Fore.GREEN}{value:<25}{Style.RESET_ALL} - {display}") + print(f"\n{Fore.YELLOW}Usage examples:{Style.RESET_ALL}") + print(f" --analysts warren_buffett,michael_burry") + print(f" --analysts-all") + sys.exit(0) + + if args.list_models: + print(f"\n{Fore.CYAN}Available Models:{Style.RESET_ALL}") + print(f"\n{Fore.YELLOW}Cloud Models:{Style.RESET_ALL}") + for display, name, provider in LLM_ORDER: + print(f" {Fore.GREEN}{name:<25}{Style.RESET_ALL} - {display} ({provider})") + + print(f"\n{Fore.YELLOW}Ollama Models:{Style.RESET_ALL}") + for display, name, _ in OLLAMA_LLM_ORDER: + print(f" {Fore.GREEN}{name:<25}{Style.RESET_ALL} - {display}") + + print(f"\n{Fore.YELLOW}Usage examples:{Style.RESET_ALL}") + print(f" --model gpt-4o") + print(f" --model claude-3-5-sonnet-20241022") + print(f" --ollama --model llama3") + sys.exit(0) + + # Check if tickers are required + if not args.tickers: + print(f"{Fore.RED}Error: --tickers is required for trading operations{Style.RESET_ALL}") + print("Use --list-analysts or --list-models to see available options") + sys.exit(1) + + + # Parse tickers + tickers = [ticker.strip().upper() for ticker in args.tickers.split(",")] + print(f"\n{Fore.CYAN}Trading tickers: {', '.join(tickers)}{Style.RESET_ALL}") + + + # Select analysts + selected_analysts = None + if args.analysts_all: + selected_analysts = [a[1] for a in ANALYST_ORDER] + elif args.analysts: + selected_analysts = [a.strip() for a in args.analysts.split(",") if a.strip()] + else: + selected_analysts = questionary.checkbox( + "Select your AI analysts:", + choices=[questionary.Choice(display, value=value) for display, value in ANALYST_ORDER], + instruction="\n\nInstructions:\n1. Press Space to select/unselect analysts\n2. Press 'a' to select/unselect all\n3. Press Enter when done\n", + validate=lambda x: len(x) > 0 or "You must select at least one analyst.", + style=questionary.Style([ + ("checkbox-selected", "fg:green"), + ("selected", "fg:green noinherit"), + ("highlighted", "noinherit"), + ("pointer", "noinherit"), + ]) + ).ask() + + if not selected_analysts: + print("\nExiting...") + sys.exit(0) + + print(f"\nSelected analysts: {', '.join(Fore.GREEN + choice.title().replace('_', ' ') + Style.RESET_ALL for choice in selected_analysts)}") + + # Select LLM model + model_name = "" + model_provider = "" + + if args.model: + # Model specified via CLI + model_name = args.model + if args.ollama: + model_provider = ModelProvider.OLLAMA.value + if not ensure_ollama_and_model(model_name): + print(f"{Fore.RED}Cannot proceed without Ollama and the selected model{Style.RESET_ALL}") + sys.exit(1) + else: + # Try to find the model in the LLM_ORDER + found = False + for display, name, provider in LLM_ORDER: + if name == model_name: + model_provider = provider + found = True + break + if not found: + print(f"{Fore.RED}Model '{model_name}' not found in available models{Style.RESET_ALL}") + sys.exit(1) + print(f"\nUsing model: {Fore.GREEN + Style.BRIGHT}{model_name}{Style.RESET_ALL} ({model_provider})") + + elif args.ollama: + print(f"{Fore.CYAN}Using Ollama for local LLM inference{Style.RESET_ALL}") + + model_name = questionary.select( + "Select your Ollama model:", + choices=[questionary.Choice(display, value=value) for display, value, _ in OLLAMA_LLM_ORDER], + style=questionary.Style([ + ("selected", "fg:green bold"), + ("pointer", "fg:green bold"), + ("highlighted", "fg:green"), + ("answer", "fg:green bold"), + ]) + ).ask() + + if not model_name: + print("\nExiting...") + sys.exit(0) + + if model_name == "-": + model_name = questionary.text("Enter the custom model name:").ask() + if not model_name: + print("\nExiting...") + sys.exit(0) + + if not ensure_ollama_and_model(model_name): + print(f"{Fore.RED}Cannot proceed without Ollama and the selected model{Style.RESET_ALL}") + sys.exit(1) + + model_provider = ModelProvider.OLLAMA.value + print(f"\nSelected {Fore.CYAN}Ollama{Style.RESET_ALL} model: {Fore.GREEN + Style.BRIGHT}{model_name}{Style.RESET_ALL}") + + else: + model_choice = questionary.select( + "Select your LLM model:", + choices=[questionary.Choice(display, value=(name, provider)) for display, name, provider in LLM_ORDER], + style=questionary.Style([ + ("selected", "fg:green bold"), + ("pointer", "fg:green bold"), + ("highlighted", "fg:green"), + ("answer", "fg:green bold"), + ]) + ).ask() + + if not model_choice: + print("\nExiting...") + sys.exit(0) + + model_name, model_provider = model_choice + + model_info = get_model_info(model_name, model_provider) + if model_info and model_info.is_custom(): + model_name = questionary.text("Enter the custom model name:").ask() + if not model_name: + print("\nExiting...") + sys.exit(0) + + print(f"\nSelected {Fore.CYAN}{model_provider}{Style.RESET_ALL} model: {Fore.GREEN + Style.BRIGHT}{model_name}{Style.RESET_ALL}") + + # Create trader + print(f"\n{Fore.CYAN}Creating trader...{Style.RESET_ALL}") + trader = create_trader( + tickers=tickers, + selected_analysts=selected_analysts, + model_name=model_name, + model_provider=model_provider, + available_capital=args.available_capital, + margin_requirement=args.margin_requirement, + dry_run=args.dry_run, + ignore_market_hours=args.ignore_market_hours + ) + + # Connect to broker + print(f"\n{Fore.CYAN}Connecting to broker...{Style.RESET_ALL}") + if not trader.connect(): + print(f"{Fore.RED}Failed to connect to broker{Style.RESET_ALL}") + sys.exit(1) + + # Get margin requirement from broker if not specified + if args.margin_requirement is None: + try: + # Get account default margin requirement + broker_margin_req = trader.broker.get_margin_requirement() + trader.margin_requirement = broker_margin_req + print(f"{Fore.CYAN}Using broker default margin requirement: {broker_margin_req:.1%}{Style.RESET_ALL}") + + # Show ticker-specific margin requirements + print(f"{Fore.CYAN}Ticker-specific margin requirements:{Style.RESET_ALL}") + for ticker in tickers: + try: + ticker_margin = trader.broker.get_margin_requirement(ticker) + print(f" {ticker}: {ticker_margin:.1%}") + except Exception as e: + print(f" {ticker}: {broker_margin_req:.1%} (default - could not get specific requirement)") + except Exception as e: + trader.margin_requirement = 0.5 # Default fallback + print(f"{Fore.YELLOW}Could not get broker margin requirement, using default 50%{Style.RESET_ALL}") + + # Show capital usage information + if args.available_capital: + print(f"{Fore.CYAN}Using limited capital: ${args.available_capital:,.2f} (maintaining account margin capability){Style.RESET_ALL}") + + # Check for live trading warning after connection + if not trader.broker.is_paper_trading(): + confirm = questionary.confirm( + f"{Fore.RED}WARNING: Connected to LIVE TRADING account with real money. Are you sure you want to continue?{Style.RESET_ALL}", + default=False + ).ask() + if not confirm: + print("Exiting...") + trader.disconnect() + sys.exit(0) + + try: + # Show portfolio summary + trader.print_portfolio_summary() + + # Run trading + if args.continuous: + print(f"\n{Fore.BLUE}Starting continuous trading mode...{Style.RESET_ALL}") + trader.run_continuous_trading(interval_minutes=args.interval) + else: + print(f"\n{Fore.BLUE}Running single trading session...{Style.RESET_ALL}") + trader.run_trading_session() + + # Show updated portfolio + trader.print_portfolio_summary() + + except KeyboardInterrupt: + print(f"\n{Fore.YELLOW}Trading interrupted by user{Style.RESET_ALL}") + except Exception as e: + print(f"\n{Fore.RED}Trading failed: {e}{Style.RESET_ALL}") + sys.exit(1) + finally: + trader.disconnect() + + print(f"\n{Fore.GREEN}Trading session completed{Style.RESET_ALL}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/trading/__init__.py b/src/trading/__init__.py new file mode 100644 index 000000000..a358e2038 --- /dev/null +++ b/src/trading/__init__.py @@ -0,0 +1 @@ +"""Trading module for real broker integration.""" \ No newline at end of file diff --git a/src/trading/alpaca_broker.py b/src/trading/alpaca_broker.py new file mode 100644 index 000000000..2b93fabf2 --- /dev/null +++ b/src/trading/alpaca_broker.py @@ -0,0 +1,390 @@ +"""Alpaca broker implementation for live trading.""" + +import os +from alpaca.trading.client import TradingClient +from alpaca.trading.requests import MarketOrderRequest, LimitOrderRequest, StopOrderRequest +from alpaca.trading.enums import OrderSide, TimeInForce, OrderType +from alpaca.data.historical import StockHistoricalDataClient +from alpaca.data.requests import StockLatestQuoteRequest +from alpaca.common.exceptions import APIError +from colorama import Fore, Style +import logging + +from .broker_base import BrokerBase, Position, Order, Account + + +logger = logging.getLogger(__name__) + + +class AlpacaBroker(BrokerBase): + """Alpaca broker implementation.""" + + def __init__(self, paper_trading: bool = None): + """Initialize Alpaca broker. + + Args: + paper_trading: Whether to use paper trading account. If None, reads from ALPACA_PAPER env var. + """ + # Read from environment variable if not explicitly set + if paper_trading is None: + paper_trading = os.getenv("ALPACA_PAPER", "true").lower() in ("true", "1", "yes") + + self.paper_trading = paper_trading + self.trading_client = None + self.data_client = None + self._connected = False + + # Check required environment variables + required_vars = ["ALPACA_API_KEY", "ALPACA_SECRET_KEY"] + missing_vars = [var for var in required_vars if not os.getenv(var)] + + if missing_vars: + raise ValueError(f"Missing required environment variables: {', '.join(missing_vars)}. Please set these in your .env file") + + # Get API credentials from environment + self.api_key = os.getenv("ALPACA_API_KEY") + self.secret_key = os.getenv("ALPACA_SECRET_KEY") + + def connect(self) -> bool: + """Connect to Alpaca API.""" + try: + self.trading_client = TradingClient( + api_key=self.api_key, + secret_key=self.secret_key, + paper=self.paper_trading + ) + + self.data_client = StockHistoricalDataClient( + api_key=self.api_key, + secret_key=self.secret_key + ) + + # Test connection by getting account info + account = self.trading_client.get_account() + self._connected = True + + env_type = "Paper" if self.paper_trading else "Live" + print(f"{Fore.GREEN}Connected to Alpaca {env_type} Trading API{Style.RESET_ALL}") + print(f"Account Status: {account.status}") + print(f"Buying Power: ${float(account.buying_power):,.2f}") + + return True + + except APIError as e: + logger.error(f"Failed to connect to Alpaca: {e}") + print(f"{Fore.RED}Failed to connect to Alpaca: {e}{Style.RESET_ALL}") + return False + except Exception as e: + logger.error(f"Unexpected error connecting to Alpaca: {e}") + print(f"{Fore.RED}Unexpected error: {e}{Style.RESET_ALL}") + return False + + def disconnect(self) -> None: + """Disconnect from Alpaca API.""" + self.trading_client = None + self.data_client = None + self._connected = False + print(f"{Fore.YELLOW}Disconnected from Alpaca API{Style.RESET_ALL}") + + def get_account(self) -> Account: + """Get current account information.""" + if not self._connected: + raise RuntimeError("Not connected to broker") + + account = self.trading_client.get_account() + + return Account( + cash=float(account.cash), + buying_power=float(account.buying_power), + portfolio_value=float(account.portfolio_value), + equity=float(account.equity), + initial_margin=float(account.initial_margin), + maintenance_margin=float(account.maintenance_margin), + sma=float(account.sma), + day_trade_count=int(account.daytrade_count), + regt_buying_power=float(account.regt_buying_power), + daytrading_buying_power=float(account.daytrading_buying_power), + multiplier=float(account.multiplier) + ) + + def get_positions(self) -> list[Position]: + """Get current positions.""" + if not self._connected: + raise RuntimeError("Not connected to broker") + + positions = [] + alpaca_positions = self.trading_client.get_all_positions() + + for pos in alpaca_positions: + side = "long" if float(pos.qty) > 0 else "short" + + # Handle optional unrealized_pnl - calculate if missing + unrealized_pnl = 0.0 + if hasattr(pos, 'unrealized_pnl') and pos.unrealized_pnl is not None: + unrealized_pnl = float(pos.unrealized_pnl) + else: + # Calculate unrealized P&L if not provided + # unrealized_pnl = market_value - cost_basis + unrealized_pnl = float(pos.market_value) - float(pos.cost_basis) + + positions.append(Position( + symbol=pos.symbol, + quantity=abs(float(pos.qty)), + side=side, + avg_entry_price=float(pos.avg_entry_price), + market_value=float(pos.market_value), + unrealized_pnl=unrealized_pnl, + cost_basis=float(pos.cost_basis) + )) + + return positions + + def get_position(self, symbol: str) -> Position | None: + """Get position for a specific symbol.""" + if not self._connected: + raise RuntimeError("Not connected to broker") + + try: + pos = self.trading_client.get_open_position(symbol) + side = "long" if float(pos.qty) > 0 else "short" + + # Handle optional unrealized_pnl - calculate if missing + unrealized_pnl = 0.0 + if hasattr(pos, 'unrealized_pnl') and pos.unrealized_pnl is not None: + unrealized_pnl = float(pos.unrealized_pnl) + else: + # Calculate unrealized P&L if not provided + unrealized_pnl = float(pos.market_value) - float(pos.cost_basis) + + return Position( + symbol=pos.symbol, + quantity=abs(float(pos.qty)), + side=side, + avg_entry_price=float(pos.avg_entry_price), + market_value=float(pos.market_value), + unrealized_pnl=unrealized_pnl, + cost_basis=float(pos.cost_basis) + ) + + except APIError: + # Position doesn't exist + return None + + def place_order(self, symbol: str, quantity: float, side: str, + order_type: str = "market", limit_price: float | None = None, + stop_price: float | None = None) -> Order: + """Place a trading order.""" + if not self._connected: + raise RuntimeError("Not connected to broker") + + # Convert side to Alpaca format + if side == "buy": + order_side = OrderSide.BUY + elif side == "sell": + order_side = OrderSide.SELL + elif side == "sell_short": + order_side = OrderSide.SELL + # Note: Alpaca handles short selling automatically when selling more than owned + else: + raise ValueError(f"Invalid order side: {side}") + + # Create order request based on type + try: + if order_type == "market": + order_request = MarketOrderRequest( + symbol=symbol, + qty=quantity, + side=order_side, + time_in_force=TimeInForce.DAY + ) + elif order_type == "limit": + if limit_price is None: + raise ValueError("Limit price required for limit orders") + order_request = LimitOrderRequest( + symbol=symbol, + qty=quantity, + side=order_side, + time_in_force=TimeInForce.DAY, + limit_price=limit_price + ) + elif order_type == "stop": + if stop_price is None: + raise ValueError("Stop price required for stop orders") + order_request = StopOrderRequest( + symbol=symbol, + qty=quantity, + side=order_side, + time_in_force=TimeInForce.DAY, + stop_price=stop_price + ) + else: + raise ValueError(f"Invalid order type: {order_type}") + + alpaca_order = self.trading_client.submit_order(order_request) + + return Order( + id=str(alpaca_order.id), + symbol=alpaca_order.symbol, + quantity=float(alpaca_order.qty), + side=side, + order_type=order_type, + status=alpaca_order.status.value, + filled_price=float(alpaca_order.filled_avg_price) if alpaca_order.filled_avg_price else None, + filled_quantity=float(alpaca_order.filled_qty) if alpaca_order.filled_qty else None, + submitted_at=alpaca_order.submitted_at, + filled_at=alpaca_order.filled_at + ) + + except APIError as e: + logger.error(f"Failed to place order: {e}") + raise RuntimeError(f"Failed to place order: {e}") + + def cancel_order(self, order_id: str) -> bool: + """Cancel an order.""" + if not self._connected: + raise RuntimeError("Not connected to broker") + + try: + self.trading_client.cancel_order_by_id(order_id) + return True + except APIError as e: + logger.error(f"Failed to cancel order {order_id}: {e}") + return False + + def get_orders(self, status: str | None = None) -> list[Order]: + """Get orders.""" + if not self._connected: + raise RuntimeError("Not connected to broker") + + orders = [] + alpaca_orders = self.trading_client.get_orders() + + for order in alpaca_orders: + if status is None or order.status.value == status: + # Convert Alpaca side back to our format + side = "buy" if order.side == OrderSide.BUY else "sell" + + orders.append(Order( + id=str(order.id), + symbol=order.symbol, + quantity=float(order.qty), + side=side, + order_type=order.order_type.value, + status=order.status.value, + filled_price=float(order.filled_avg_price) if order.filled_avg_price else None, + filled_quantity=float(order.filled_qty) if order.filled_qty else None, + submitted_at=order.submitted_at, + filled_at=order.filled_at + )) + + return orders + + def get_order(self, order_id: str) -> Order | None: + """Get specific order.""" + if not self._connected: + raise RuntimeError("Not connected to broker") + + try: + order = self.trading_client.get_order_by_id(order_id) + side = "buy" if order.side == OrderSide.BUY else "sell" + + return Order( + id=str(order.id), + symbol=order.symbol, + quantity=float(order.qty), + side=side, + order_type=order.order_type.value, + status=order.status.value, + filled_price=float(order.filled_avg_price) if order.filled_avg_price else None, + filled_quantity=float(order.filled_qty) if order.filled_qty else None, + submitted_at=order.submitted_at, + filled_at=order.filled_at + ) + + except APIError: + return None + + def get_current_price(self, symbol: str) -> float: + """Get current price for a symbol.""" + if not self._connected: + raise RuntimeError("Not connected to broker") + + try: + request = StockLatestQuoteRequest(symbol_or_symbols=[symbol]) + quote = self.data_client.get_stock_latest_quote(request) + + if symbol in quote: + return float(quote[symbol].bid_price) + else: + raise ValueError(f"No quote available for {symbol}") + + except APIError as e: + logger.error(f"Failed to get price for {symbol}: {e}") + raise RuntimeError(f"Failed to get price for {symbol}: {e}") + + def is_market_open(self) -> bool: + """Check if market is currently open.""" + if not self._connected: + raise RuntimeError("Not connected to broker") + + try: + clock = self.trading_client.get_clock() + return clock.is_open + except APIError as e: + logger.error(f"Failed to get market status: {e}") + return False + + def is_paper_trading(self) -> bool: + """Check if this is a paper trading account.""" + return self.paper_trading + + def get_margin_requirement(self, symbol: str | None = None) -> float: + """Get margin requirement ratio for positions. + + Args: + symbol: Stock symbol to get specific margin requirement for. + If None, returns account default margin requirement. + + Returns: + float: Margin requirement ratio (e.g., 0.5 for 50% margin requirement). + """ + if not self._connected: + raise RuntimeError("Not connected to broker") + + try: + # If a specific symbol is requested, get its asset-specific margin requirement + if symbol: + try: + asset = self.trading_client.get_asset(symbol) + if asset and hasattr(asset, 'maintenance_margin_requirement') and asset.maintenance_margin_requirement is not None: + # Alpaca returns margin requirement as percentage points (30.0 = 30%) + margin_req = asset.maintenance_margin_requirement + margin_float = float(margin_req) + + # Convert percentage points to ratio (30.0 -> 0.30) + if margin_float > 0: + return margin_float / 100.0 + else: + # Invalid margin requirement, fall through to account default + print(f"WARNING: Invalid margin requirement {margin_float} for {symbol}, using account default") + except APIError: + # Fall through to account-level default if asset not found + pass + except Exception as e: + print(f"DEBUG: Error processing margin requirement for {symbol}: {e}") + # Fall through to account default + + # Fallback to account-level margin requirement + account = self.trading_client.get_account() + multiplier = float(account.multiplier) if account.multiplier else 1.0 + + if multiplier > 1: + # Margin account - typical requirement is 50% for most stocks + return 0.5 # 50% margin requirement for margin accounts + else: + # Cash account - cannot use margin for overnight positions + return 1.0 # 100% cash requirement for cash accounts (no margin) + + except APIError as e: + logger.error(f"Failed to get margin requirement: {e}") + return 0.5 # Default to 50% if we can't determine \ No newline at end of file diff --git a/src/trading/broker_base.py b/src/trading/broker_base.py new file mode 100644 index 000000000..9c0d16f60 --- /dev/null +++ b/src/trading/broker_base.py @@ -0,0 +1,194 @@ +"""Base broker interface for trading implementations.""" + +from abc import ABC, abstractmethod +from dataclasses import dataclass +from datetime import datetime + + +@dataclass +class Position: + """Represents a trading position.""" + symbol: str + quantity: float + side: str # "long" or "short" + avg_entry_price: float + market_value: float + unrealized_pnl: float + cost_basis: float + + +@dataclass +class Order: + """Represents a trading order.""" + id: str + symbol: str + quantity: float + side: str # "buy", "sell", "sell_short" + order_type: str # "market", "limit", "stop" + status: str # "pending", "filled", "canceled", "rejected" + filled_price: float | None = None + filled_quantity: float | None = None + submitted_at: datetime | None = None + filled_at: datetime | None = None + + +@dataclass +class Account: + """Represents account information.""" + cash: float + buying_power: float + portfolio_value: float + equity: float + initial_margin: float + maintenance_margin: float + sma: float # Special Memorandum Account + day_trade_count: int + regt_buying_power: float + daytrading_buying_power: float + multiplier: float + + +class BrokerBase(ABC): + """Abstract base class for broker implementations.""" + + @abstractmethod + def connect(self) -> bool: + """Connect to the broker API. + + Returns: + bool: True if connection successful, False otherwise. + """ + pass + + @abstractmethod + def disconnect(self) -> None: + """Disconnect from the broker API.""" + pass + + @abstractmethod + def get_account(self) -> Account: + """Get current account information. + + Returns: + Account: Current account details. + """ + pass + + @abstractmethod + def get_positions(self) -> list[Position]: + """Get current positions. + + Returns: + list[Position]: List of current positions. + """ + pass + + @abstractmethod + def get_position(self, symbol: str) -> Position | None: + """Get position for a specific symbol. + + Args: + symbol: Stock symbol. + + Returns: + Position | None: Position if exists, None otherwise. + """ + pass + + @abstractmethod + def place_order(self, symbol: str, quantity: float, side: str, + order_type: str = "market", limit_price: float | None = None, + stop_price: float | None = None) -> Order: + """Place a trading order. + + Args: + symbol: Stock symbol. + quantity: Number of shares. + side: "buy", "sell", or "sell_short". + order_type: "market", "limit", or "stop". + limit_price: Limit price for limit orders. + stop_price: Stop price for stop orders. + + Returns: + Order: The placed order. + """ + pass + + @abstractmethod + def cancel_order(self, order_id: str) -> bool: + """Cancel an order. + + Args: + order_id: Order ID to cancel. + + Returns: + bool: True if cancellation successful, False otherwise. + """ + pass + + @abstractmethod + def get_orders(self, status: str | None = None) -> list[Order]: + """Get orders. + + Args: + status: Filter by order status. None for all orders. + + Returns: + list[Order]: List of orders. + """ + pass + + @abstractmethod + def get_order(self, order_id: str) -> Order | None: + """Get specific order. + + Args: + order_id: Order ID. + + Returns: + Order | None: Order if exists, None otherwise. + """ + pass + + @abstractmethod + def get_current_price(self, symbol: str) -> float: + """Get current price for a symbol. + + Args: + symbol: Stock symbol. + + Returns: + float: Current price. + """ + pass + + @abstractmethod + def is_market_open(self) -> bool: + """Check if market is currently open. + + Returns: + bool: True if market is open, False otherwise. + """ + pass + + @abstractmethod + def is_paper_trading(self) -> bool: + """Check if this is a paper trading account. + + Returns: + bool: True if paper trading, False if live trading. + """ + pass + + @abstractmethod + def get_margin_requirement(self, symbol: str | None = None) -> float: + """Get margin requirement ratio for positions. + + Args: + symbol: Stock symbol to get specific margin requirement for. + If None, returns account default margin requirement. + + Returns: + float: Margin requirement ratio (e.g., 0.5 for 50% margin requirement). + """ + pass \ No newline at end of file diff --git a/src/trading/trader.py b/src/trading/trader.py new file mode 100644 index 000000000..4a8f4577e --- /dev/null +++ b/src/trading/trader.py @@ -0,0 +1,680 @@ +"""Live trading implementation using the hedge fund AI agents.""" + +import sys +import time +import logging +from datetime import datetime, timedelta +from dateutil.relativedelta import relativedelta +from colorama import Fore, Style + +from .broker_base import BrokerBase, Position, Order +from .alpaca_broker import AlpacaBroker +from src.main import run_hedge_fund +from src.utils.progress import progress + + +logger = logging.getLogger(__name__) + + +class Trader: + """Live trading implementation using AI hedge fund decisions.""" + + def __init__( + self, + broker: BrokerBase, + tickers: list[str], + selected_analysts: list[str] = None, + model_name: str = "gpt-4o", + model_provider: str = "OpenAI", + available_capital: float = None, # Override available capital + margin_requirement: float = None, # Margin requirement for short positions + dry_run: bool = False, + ignore_market_hours: bool = False + ): + """Initialize the trader. + + Args: + broker: Broker implementation to use. + tickers: List of tickers to trade. + selected_analysts: List of analysts to use. + model_name: LLM model name. + model_provider: LLM provider. + available_capital: Override available capital (uses broker cash if None). + margin_requirement: Margin requirement ratio for short positions. + dry_run: If True, log trades but don't execute them. + ignore_market_hours: If True, run even when market is closed. + """ + self.broker = broker + self.tickers = tickers + self.selected_analysts = selected_analysts or [] + self.model_name = model_name + self.model_provider = model_provider + self.available_capital = available_capital + self.margin_requirement = margin_requirement + self.dry_run = dry_run + self.ignore_market_hours = ignore_market_hours + + # Track our virtual portfolio for decision making + self.virtual_portfolio = None + self.last_sync_time = None + + def _map_order_to_action(self, order: Order, symbol: str) -> str: + """Map a broker order to our action type.""" + if order.side == "buy": + # Could be buy or cover - check if we have short positions + current_pos = self.broker.get_position(symbol) + if current_pos and current_pos.side == "short": + return "cover" + else: + return "buy" + elif order.side == "sell": + # Could be sell or short - check if we have long positions + current_pos = self.broker.get_position(symbol) + if current_pos and current_pos.side == "long": + return "sell" + else: + return "short" + else: + return "unknown" + + def _close_position_and_wait(self, symbol: str, position_type: str, position_size: float) -> Order: + """Close existing position and wait for fill. + + Args: + symbol: Stock symbol + position_type: 'long' or 'short' + position_size: Number of shares to close + + Returns: + Order: The close order + """ + if position_type == "long": + action_name = "close long" + order_side = "sell" + else: # short + action_name = "close short" + order_side = "buy" + + print(f"{Fore.CYAN}Step 1: {action_name.title()} - {order_side} {position_size} shares{Style.RESET_ALL}") + close_order = self.broker.place_order(symbol, position_size, order_side) + print(f"{Fore.GREEN}Close order placed: {close_order.id}{Style.RESET_ALL}") + + # Wait for close order to fill + self._wait_for_order_fill(close_order.id, action_name) + return close_order + + def _wait_for_order_fill(self, order_id: str, action_name: str) -> None: + """Wait for order to fill with timeout. + + Args: + order_id: Order ID to monitor + action_name: Human-readable action name for logging + """ + print(f"{Fore.CYAN}Waiting for {action_name} order to fill...{Style.RESET_ALL}") + max_wait_time = 30 # seconds + wait_time = 0 + while wait_time < max_wait_time: + updated_order = self.broker.get_order(order_id) + if updated_order and updated_order.status == "filled": + print(f"{Fore.GREEN}{action_name.title()} order filled successfully{Style.RESET_ALL}") + return + time.sleep(2) + wait_time += 2 + + print(f"{Fore.YELLOW}{action_name.title()} order still pending after {max_wait_time}s, proceeding anyway{Style.RESET_ALL}") + + def _execute_position_transition(self, symbol: str, action: str, quantity: float, + current_long: float, current_short: float) -> Order: + """Execute position transition with proper sequencing. + + Args: + symbol: Stock symbol + action: Target action (buy/sell/short/cover) + quantity: Total quantity for the action + current_long: Current long position size + current_short: Current short position size + + Returns: + Order: The final order executed + """ + if action == "buy" and current_short > 0: + # Short -> Long transition + print(f"{Fore.CYAN}Detected short position of {current_short} shares, splitting buy order{Style.RESET_ALL}") + + # Close short position + cover_quantity = min(quantity, current_short) + close_order = self._close_position_and_wait(symbol, "short", cover_quantity) + + # Buy remaining for long position + remaining_quantity = quantity - cover_quantity + if remaining_quantity > 0: + print(f"{Fore.CYAN}Step 2: Buying {remaining_quantity} additional shares for long position{Style.RESET_ALL}") + return self.broker.place_order(symbol, remaining_quantity, "buy") + else: + print(f"{Fore.CYAN}Short position fully covered, no additional long position needed{Style.RESET_ALL}") + return close_order + + elif action == "sell" and current_long > 0 and quantity > current_long: + # Long -> Short transition (selling more than owned) + print(f"{Fore.CYAN}Detected long position of {current_long} shares, splitting sell order{Style.RESET_ALL}") + + # Close long position + close_order = self._close_position_and_wait(symbol, "long", current_long) + + # Short sell remaining + remaining_quantity = quantity - current_long + print(f"{Fore.CYAN}Step 2: Short selling {remaining_quantity} additional shares{Style.RESET_ALL}") + return self.broker.place_order(symbol, remaining_quantity, "sell_short") + + elif action == "short" and current_long > 0: + # Long -> Short transition (close long first) + print(f"{Fore.CYAN}Detected long position of {current_long} shares, closing before short sale{Style.RESET_ALL}") + + # Close long position + self._close_position_and_wait(symbol, "long", current_long) + + # Short sell the requested quantity + print(f"{Fore.CYAN}Step 2: Short selling {quantity} shares{Style.RESET_ALL}") + return self.broker.place_order(symbol, quantity, "sell_short") + + elif action == "cover" and current_short > 0 and quantity > current_short: + # Short -> Long transition (covering more than short) + print(f"{Fore.CYAN}Detected short position of {current_short} shares, splitting cover order{Style.RESET_ALL}") + + # Cover short position + self._close_position_and_wait(symbol, "short", current_short) + + # Buy remaining for long position + remaining_quantity = quantity - current_short + print(f"{Fore.CYAN}Step 2: Buying {remaining_quantity} additional shares for long position{Style.RESET_ALL}") + return self.broker.place_order(symbol, remaining_quantity, "buy") + + else: + # No position transition needed - execute normal order + return self._execute_simple_order(symbol, action, quantity, current_long, current_short) + + def _execute_simple_order(self, symbol: str, action: str, quantity: float, + current_long: float, current_short: float) -> Order | None: + """Execute simple order without position transitions. + + Args: + symbol: Stock symbol + action: Trading action + quantity: Number of shares + current_long: Current long position + current_short: Current short position + + Returns: + Order or None if cannot execute + """ + if action == "buy": + return self.broker.place_order(symbol, quantity, "buy") + elif action == "sell": + if current_long > 0: + return self.broker.place_order(symbol, quantity, "sell") + else: + print(f"{Fore.YELLOW}Cannot sell {symbol}: no long position{Style.RESET_ALL}") + return None + elif action == "short": + return self.broker.place_order(symbol, quantity, "sell_short") + elif action == "cover": + if current_short > 0: + return self.broker.place_order(symbol, quantity, "buy") + else: + print(f"{Fore.YELLOW}Cannot cover {symbol}: no short position{Style.RESET_ALL}") + return None + else: + print(f"{Fore.RED}Unknown action: {action}{Style.RESET_ALL}") + return None + + def _handle_hold_decision(self, symbol: str) -> Order | None: + """Handle AI decision to hold - cancel any pending orders.""" + if self.dry_run: + print(f"{Fore.CYAN}DRY RUN: Would hold {symbol} (cancel any pending orders){Style.RESET_ALL}") + return None + + # Get pending orders and cancel them + pending_orders = [order for order in self.broker.get_orders() + if order.symbol == symbol and order.status in ["pending_new", "new", "accepted"]] + + if pending_orders: + print(f"{Fore.CYAN}AI says hold {symbol}, canceling {len(pending_orders)} pending orders{Style.RESET_ALL}") + for order in pending_orders: + action = self._map_order_to_action(order, symbol) + print(f"{Fore.YELLOW}Canceling pending {action} order for {order.quantity} shares{Style.RESET_ALL}") + if self.broker.cancel_order(order.id): + print(f"{Fore.GREEN}Canceled order {order.id}{Style.RESET_ALL}") + else: + print(f"{Fore.RED}Failed to cancel order {order.id}{Style.RESET_ALL}") + else: + print(f"{Fore.CYAN}Holding {symbol} (no pending orders to cancel){Style.RESET_ALL}") + + return None + + def connect(self) -> bool: + """Connect to the broker.""" + return self.broker.connect() + + def disconnect(self) -> None: + """Disconnect from the broker.""" + self.broker.disconnect() + + def sync_portfolio(self) -> None: + """Sync virtual portfolio with actual broker positions.""" + # Temporarily stop progress display to show debug output + was_started = progress.started + if was_started: + progress.stop() + + try: + # Ensure broker connection is active with retry + max_retries = 3 + for attempt in range(max_retries): + try: + if not self.broker._connected: + print(f"{Fore.YELLOW}Reconnecting to broker (attempt {attempt + 1})...{Style.RESET_ALL}") + self.broker.connect() + + account = self.broker.get_account() + positions = self.broker.get_positions() + pending_orders = self.broker.get_orders() if not self.dry_run else [] + break # Success, exit retry loop + + except Exception as e: + if attempt < max_retries - 1: + print(f"{Fore.YELLOW}Connection failed, retrying in 5 seconds... ({e}){Style.RESET_ALL}") + time.sleep(5) + self.broker._connected = False # Force reconnection + else: + raise # Re-raise if all retries failed + + # Create virtual portfolio structure similar to backtester + # Use limited capital if specified, but keep the account's margin capability + actual_cash = account.cash + limited_cash = self.available_capital if self.available_capital is not None else actual_cash + + # Calculate the fraction of available capital we're using + capital_fraction = limited_cash / actual_cash if actual_cash > 0 else 1.0 + + self.virtual_portfolio = { + "cash": limited_cash, + "margin_requirement": self.margin_requirement if self.margin_requirement is not None else self.broker.get_margin_requirement(), + "margin_used": account.initial_margin * capital_fraction, # Scale margin usage proportionally + "positions": {}, + "realized_gains": {}, + "pending_orders": {} # Track pending orders + } + + # Initialize all tickers + for ticker in self.tickers: + self.virtual_portfolio["positions"][ticker] = { + "long": 0, + "short": 0, + "long_cost_basis": 0.0, + "short_cost_basis": 0.0, + "short_margin_used": 0.0, + } + self.virtual_portfolio["realized_gains"][ticker] = { + "long": 0.0, + "short": 0.0, + } + self.virtual_portfolio["pending_orders"][ticker] = { + "buy": 0, + "sell": 0, + "short": 0, + "cover": 0, + } + + # Update with actual positions + print(f"{Fore.YELLOW}DEBUG: Syncing {len(positions)} positions from broker{Style.RESET_ALL}") + for position in positions: + print(f"DEBUG: Position {position.symbol}: {position.side} {position.quantity} shares") + if position.symbol in self.virtual_portfolio["positions"]: + if position.side == "long": + self.virtual_portfolio["positions"][position.symbol]["long"] = position.quantity + self.virtual_portfolio["positions"][position.symbol]["long_cost_basis"] = position.avg_entry_price + print(f"DEBUG: Updated {position.symbol} long: {position.quantity} shares") + else: # short + self.virtual_portfolio["positions"][position.symbol]["short"] = position.quantity + self.virtual_portfolio["positions"][position.symbol]["short_cost_basis"] = position.avg_entry_price + print(f"DEBUG: Updated {position.symbol} short: {position.quantity} shares") + else: + print(f"DEBUG: Skipping {position.symbol} - not in tickers list") + + # Debug: Show what the virtual portfolio looks like after sync + print(f"{Fore.YELLOW}DEBUG: Virtual portfolio positions after sync:{Style.RESET_ALL}") + for ticker in self.tickers: + pos = self.virtual_portfolio["positions"][ticker] + print(f" {ticker}: long={pos['long']}, short={pos['short']}") + + # Track pending orders + for order in pending_orders: + if order.symbol in self.virtual_portfolio["pending_orders"] and order.status in ["pending_new", "new", "accepted"]: + # Map order sides to our action types + if order.side == "buy": + action = "buy" + elif order.side == "sell": + # Need to determine if this is a sell or cover based on current position + current_pos = self.virtual_portfolio["positions"].get(order.symbol, {}) + if current_pos.get("short", 0) > 0: + action = "cover" + else: + action = "sell" + else: + action = "short" # sell_short + + self.virtual_portfolio["pending_orders"][order.symbol][action] += order.quantity + print(f"{Fore.CYAN}Found pending {action} order for {order.symbol}: {order.quantity} shares{Style.RESET_ALL}") + + self.last_sync_time = datetime.now() + print(f"{Fore.GREEN}Portfolio synced with broker{Style.RESET_ALL}") + + except Exception as e: + logger.error(f"Failed to sync portfolio: {e}") + print(f"{Fore.RED}Failed to sync portfolio: {e}{Style.RESET_ALL}") + raise + finally: + # Restart progress display if it was running + if was_started: + progress.start() + + def get_hedge_fund_decisions(self) -> dict: + """Get trading decisions from the AI hedge fund.""" + if not self.virtual_portfolio: + raise RuntimeError("Portfolio not synced. Call sync_portfolio() first.") + + # Use 30-day lookback period + end_date = datetime.now().strftime("%Y-%m-%d") + start_date = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d") + + print(f"{Fore.CYAN}Getting AI hedge fund decisions...{Style.RESET_ALL}") + + try: + result = run_hedge_fund( + tickers=self.tickers, + start_date=start_date, + end_date=end_date, + portfolio=self.virtual_portfolio, + show_reasoning=False, + selected_analysts=self.selected_analysts, + model_name=self.model_name, + model_provider=self.model_provider, + ) + + return result + + except Exception as e: + logger.error(f"Failed to get hedge fund decisions: {e}") + print(f"{Fore.RED}Failed to get hedge fund decisions: {e}{Style.RESET_ALL}") + raise + + def calculate_position_size(self, symbol: str, decision: dict) -> float: + """Get position size from AI decision (Risk Management Agent handles sizing).""" + if not self.virtual_portfolio: + raise RuntimeError("Portfolio not synced") + + # Trust the Risk Management Agent - just return what it calculated + requested_quantity = decision.get("quantity", 0) + return int(requested_quantity) if requested_quantity > 0 else 0 + + def execute_trade(self, symbol: str, decision: dict) -> Order | None: + """Execute a single trade based on AI decision.""" + action = decision.get("action", "hold") + + if action == "hold": + return self._handle_hold_decision(symbol) + + # Calculate position size + quantity = self.calculate_position_size(symbol, decision) + if quantity <= 0: + print(f"{Fore.YELLOW}Skipping {symbol}: quantity too small{Style.RESET_ALL}") + return None + + # Check for existing pending orders and handle them + if not self.dry_run: + existing_orders = [order for order in self.broker.get_orders() + if order.symbol == symbol and order.status in ["pending_new", "new", "accepted"]] + + for existing_order in existing_orders: + # Determine the action of the existing order + existing_action = self._map_order_to_action(existing_order, symbol) + + # If same action and same quantity, skip + if existing_action == action and existing_order.quantity == quantity: + print(f"{Fore.CYAN}Skipping {symbol}: Identical {action} order for {quantity} shares already pending{Style.RESET_ALL}") + return None + + # Different action or quantity - cancel the existing order + print(f"{Fore.YELLOW}Canceling existing {existing_action} order for {existing_order.quantity} shares to place new {action} order for {quantity} shares{Style.RESET_ALL}") + if self.broker.cancel_order(existing_order.id): + print(f"{Fore.GREEN}Canceled order {existing_order.id}{Style.RESET_ALL}") + else: + print(f"{Fore.RED}Failed to cancel order {existing_order.id}{Style.RESET_ALL}") + + # Get current position + current_position = self.broker.get_position(symbol) + current_long = current_position.quantity if current_position and current_position.side == "long" else 0 + current_short = current_position.quantity if current_position and current_position.side == "short" else 0 + + print(f"{Fore.CYAN}Executing {action} {quantity} shares of {symbol}{Style.RESET_ALL}") + + if self.dry_run: + print(f"{Fore.YELLOW}DRY RUN: Would {action} {quantity} shares of {symbol} at current price{Style.RESET_ALL}") + # Create a mock order for dry run + return Order( + id=f"DRY_RUN_{symbol}_{action}_{int(time.time())}", + symbol=symbol, + quantity=quantity, + side=action, + order_type="market", + status="filled", + filled_price=0.0, # Would need current price in real scenario + filled_quantity=quantity + ) + + try: + # Double-check dry run protection + if self.dry_run: + print(f"{Fore.RED}ERROR: Attempted to place real order in dry-run mode! This should not happen.{Style.RESET_ALL}") + return None + + # Execute order with smart position transition handling + order = self._execute_position_transition(symbol, action, quantity, current_long, current_short) + + if order: + print(f"{Fore.GREEN}Order placed: {order.id} - {action} {order.quantity} {symbol}{Style.RESET_ALL}") + return order + + except Exception as e: + logger.error(f"Failed to execute trade for {symbol}: {e}") + print(f"{Fore.RED}Failed to execute trade for {symbol}: {e}{Style.RESET_ALL}") + return None + + def run_trading_session(self) -> None: + """Run a single trading session.""" + print(f"{Fore.BLUE}{'='*50}") + print(f"Starting trading session at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + print(f"{'='*50}{Style.RESET_ALL}") + + if self.dry_run: + print(f"{Fore.YELLOW}🔄 DRY RUN MODE - No real trades will be executed{Style.RESET_ALL}") + + try: + # Check if market is open (unless ignoring market hours) + if not self.ignore_market_hours and not self.broker.is_market_open(): + print(f"{Fore.YELLOW}Market is closed. Skipping trading session.{Style.RESET_ALL}") + print(f"{Fore.CYAN}Use --ignore-market-hours to run anyway{Style.RESET_ALL}") + return + elif self.ignore_market_hours and not self.broker.is_market_open(): + print(f"{Fore.YELLOW}Market is closed, but ignoring market hours as requested{Style.RESET_ALL}") + + # Sync portfolio with broker + print(f"{Fore.CYAN}DEBUG: About to sync portfolio...{Style.RESET_ALL}") + self.sync_portfolio() + print(f"{Fore.CYAN}DEBUG: Portfolio sync completed{Style.RESET_ALL}") + + # Get AI decisions + result = self.get_hedge_fund_decisions() + decisions = result.get("decisions", {}) + analyst_signals = result.get("analyst_signals", {}) + + # Display decisions + print(f"\n{Fore.WHITE}{Style.BRIGHT}AI HEDGE FUND DECISIONS:{Style.RESET_ALL}") + for ticker, decision in decisions.items(): + action = decision.get("action", "hold") + quantity = decision.get("quantity", 0) + reasoning = decision.get("reasoning", "No reasoning provided") + + color = { + "buy": Fore.GREEN, + "sell": Fore.RED, + "short": Fore.MAGENTA, + "cover": Fore.CYAN, + "hold": Fore.YELLOW + }.get(action, Fore.WHITE) + + print(f"{color}{ticker}: {action.upper()} {quantity} shares{Style.RESET_ALL}") + print(f" Reasoning: {reasoning[:100]}...") + + # Execute trades + executed_orders = [] + print(f"\n{Fore.WHITE}{Style.BRIGHT}EXECUTING TRADES:{Style.RESET_ALL}") + + for ticker, decision in decisions.items(): + order = self.execute_trade(ticker, decision) + if order: + executed_orders.append(order) + + # Wait for orders to fill (basic implementation) + if executed_orders and not self.dry_run: + print(f"\n{Fore.CYAN}Monitoring order execution...{Style.RESET_ALL}") + time.sleep(5) # Wait a bit for market orders to fill + + for order in executed_orders: + updated_order = self.broker.get_order(order.id) + if updated_order: + status_color = Fore.GREEN if updated_order.status == "filled" else Fore.YELLOW + print(f"{status_color}Order {order.id}: {updated_order.status}{Style.RESET_ALL}") + + print(f"\n{Fore.GREEN}Trading session completed{Style.RESET_ALL}") + sys.stdout.flush() + + except Exception as e: + logger.error(f"Trading session failed: {e}") + print(f"{Fore.RED}Trading session failed: {e}{Style.RESET_ALL}") + + def run_continuous_trading(self, interval_minutes: int = 60) -> None: + """Run continuous trading with specified interval.""" + print(f"{Fore.BLUE}Starting continuous trading (interval: {interval_minutes} minutes){Style.RESET_ALL}") + + try: + while True: + self.run_trading_session() + + print(f"\n{Fore.CYAN}Waiting {interval_minutes} minutes until next session...{Style.RESET_ALL}") + time.sleep(interval_minutes * 60) + + except KeyboardInterrupt: + print(f"\n{Fore.YELLOW}Trading stopped by user{Style.RESET_ALL}") + except Exception as e: + logger.error(f"Continuous trading failed: {e}") + print(f"{Fore.RED}Continuous trading failed: {e}{Style.RESET_ALL}") + + def print_portfolio_summary(self) -> None: + """Print current portfolio summary.""" + try: + account = self.broker.get_account() + positions = self.broker.get_positions() + pending_orders = self.broker.get_orders(status="pending_new") if not self.dry_run else [] + open_orders = self.broker.get_orders() if not self.dry_run else [] + + print(f"\n{Fore.WHITE}{Style.BRIGHT}PORTFOLIO SUMMARY:{Style.RESET_ALL}") + print(f"Portfolio Value: ${account.portfolio_value:,.2f}") + print(f"Cash: ${account.cash:,.2f}") + print(f"Buying Power: ${account.buying_power:,.2f}") + print(f"Day Trade Count: {account.day_trade_count}") + + if positions: + print(f"\n{Fore.WHITE}{Style.BRIGHT}POSITIONS:{Style.RESET_ALL}") + for pos in positions: + color = Fore.GREEN if pos.unrealized_pnl >= 0 else Fore.RED + print(f"{pos.symbol}: {pos.side} {pos.quantity:,.0f} shares @ ${pos.avg_entry_price:.2f}") + print(f" Market Value: ${pos.market_value:,.2f}") + print(f" {color}Unrealized P&L: ${pos.unrealized_pnl:,.2f}{Style.RESET_ALL}") + else: + print(f"\n{Fore.YELLOW}No open positions{Style.RESET_ALL}") + + # Show pending/open orders + if open_orders: + print(f"\n{Fore.WHITE}{Style.BRIGHT}ORDERS:{Style.RESET_ALL}") + for order in open_orders: + status_color = { + "pending_new": Fore.YELLOW, + "new": Fore.CYAN, + "partially_filled": Fore.BLUE, + "filled": Fore.GREEN, + "done_for_day": Fore.MAGENTA, + "canceled": Fore.RED, + "expired": Fore.RED, + "replaced": Fore.YELLOW, + "pending_cancel": Fore.YELLOW, + "pending_replace": Fore.YELLOW, + "accepted": Fore.CYAN, + "accepted_for_bidding": Fore.CYAN, + "stopped": Fore.RED, + "rejected": Fore.RED, + "suspended": Fore.RED + }.get(order.status, Fore.WHITE) + + print(f"{order.symbol}: {status_color}{order.status.upper()}{Style.RESET_ALL} - {order.side} {order.quantity} @ {order.order_type}") + print(f" Order ID: {order.id}") + if order.submitted_at: + print(f" Submitted: {order.submitted_at.strftime('%Y-%m-%d %H:%M:%S')}") + if order.filled_quantity and order.filled_quantity > 0: + print(f" Filled: {order.filled_quantity}/{order.quantity} @ ${order.filled_price:.2f}") + else: + print(f"\n{Fore.YELLOW}No pending orders{Style.RESET_ALL}") + + except Exception as e: + logger.error(f"Failed to get portfolio summary: {e}") + print(f"{Fore.RED}Failed to get portfolio summary: {e}{Style.RESET_ALL}") + + +def create_trader( + tickers: list[str], + selected_analysts: list[str] = None, + model_name: str = "gpt-4o", + model_provider: str = "OpenAI", + available_capital: float = None, + margin_requirement: float = None, + dry_run: bool = False, + ignore_market_hours: bool = False +) -> Trader: + """Create a configured trader instance. + + Args: + tickers: List of tickers to trade. + selected_analysts: List of analysts to use. + model_name: LLM model name. + model_provider: LLM provider. + available_capital: Override available capital (uses broker cash if None). + margin_requirement: Margin requirement ratio for short positions. + dry_run: If True, log trades but don't execute them. + ignore_market_hours: If True, run even when market is closed. + + Returns: + Trader: Configured trader instance. + """ + broker = AlpacaBroker() # Will read ALPACA_PAPER from environment + + return Trader( + broker=broker, + tickers=tickers, + selected_analysts=selected_analysts, + model_name=model_name, + model_provider=model_provider, + available_capital=available_capital, + margin_requirement=margin_requirement, + dry_run=dry_run, + ignore_market_hours=ignore_market_hours + ) \ No newline at end of file