From 1cdfe3973a45a52d64bb74747d5218e8778d0be5 Mon Sep 17 00:00:00 2001 From: Ajasra Date: Tue, 20 May 2025 16:22:11 +0800 Subject: [PATCH 1/6] Uv package manager --- .python-version | 1 + pyproject.toml | 13 + uv.lock | 807 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 821 insertions(+) create mode 100644 .python-version create mode 100644 pyproject.toml create mode 100644 uv.lock diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..c8cfe39 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.10 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8f0fb93 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,13 @@ +[project] +name = "cycles" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "gspread>=6.2.1", + "matplotlib>=3.10.3", + "pandas>=2.2.3", + "psutil>=7.0.0", + "scipy>=1.15.3", +] diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..525bb99 --- /dev/null +++ b/uv.lock @@ -0,0 +1,807 @@ +version = 1 +revision = 2 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version < '3.11'", +] + +[[package]] +name = "cachetools" +version = "5.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380, upload-time = "2025-02-20T21:01:19.524Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080, upload-time = "2025-02-20T21:01:16.647Z" }, +] + +[[package]] +name = "certifi" +version = "2025.4.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818, upload-time = "2025-05-02T08:31:46.725Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649, upload-time = "2025-05-02T08:31:48.889Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045, upload-time = "2025-05-02T08:31:50.757Z" }, + { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356, upload-time = "2025-05-02T08:31:52.634Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471, upload-time = "2025-05-02T08:31:56.207Z" }, + { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317, upload-time = "2025-05-02T08:31:57.613Z" }, + { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368, upload-time = "2025-05-02T08:31:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491, upload-time = "2025-05-02T08:32:01.219Z" }, + { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695, upload-time = "2025-05-02T08:32:03.045Z" }, + { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849, upload-time = "2025-05-02T08:32:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091, upload-time = "2025-05-02T08:32:06.719Z" }, + { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445, upload-time = "2025-05-02T08:32:08.66Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782, upload-time = "2025-05-02T08:32:10.46Z" }, + { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" }, + { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" }, + { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" }, + { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" }, + { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" }, + { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" }, + { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, + { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, + { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, + { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, + { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, + { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, + { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, + { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" }, + { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" }, + { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" }, + { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" }, + { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" }, + { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" }, + { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" }, + { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" }, + { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" }, + { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" }, + { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" }, + { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" }, + { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload-time = "2025-04-15T17:38:19.142Z" }, + { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload-time = "2025-04-15T17:38:23.688Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload-time = "2025-04-15T17:38:28.238Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload-time = "2025-04-15T17:38:33.502Z" }, + { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload-time = "2025-04-15T17:38:38.672Z" }, + { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload-time = "2025-04-15T17:38:43.712Z" }, + { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload-time = "2025-04-15T17:39:00.224Z" }, + { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload-time = "2025-04-15T17:43:29.649Z" }, + { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload-time = "2025-04-15T17:44:44.532Z" }, + { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload-time = "2025-04-15T17:44:48.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload-time = "2025-04-15T17:43:34.084Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload-time = "2025-04-15T17:43:38.626Z" }, + { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload-time = "2025-04-15T17:43:44.522Z" }, + { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload-time = "2025-04-15T17:43:49.545Z" }, + { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload-time = "2025-04-15T17:43:54.203Z" }, + { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload-time = "2025-04-15T17:44:01.025Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload-time = "2025-04-15T17:44:17.322Z" }, + { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload-time = "2025-04-15T17:44:33.43Z" }, + { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload-time = "2025-04-15T17:44:37.092Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload-time = "2025-04-15T17:44:40.827Z" }, + { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" }, + { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" }, + { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" }, + { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "cycles" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "gspread" }, + { name = "matplotlib" }, + { name = "pandas" }, + { name = "psutil" }, + { name = "scipy" }, +] + +[package.metadata] +requires-dist = [ + { name = "gspread", specifier = ">=6.2.1" }, + { name = "matplotlib", specifier = ">=3.10.3" }, + { name = "pandas", specifier = ">=2.2.3" }, + { name = "psutil", specifier = ">=7.0.0" }, + { name = "scipy", specifier = ">=1.15.3" }, +] + +[[package]] +name = "fonttools" +version = "4.58.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/cf/4d037663e2a1fe30fddb655d755d76e18624be44ad467c07412c2319ab97/fonttools-4.58.0.tar.gz", hash = "sha256:27423d0606a2c7b336913254bf0b1193ebd471d5f725d665e875c5e88a011a43", size = 3514522, upload-time = "2025-05-10T17:36:35.886Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/07/06d01b7239d6632a0984ef29ab496928531862b827cd3aa78309b205850d/fonttools-4.58.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0bcaa65cddbc7d32c77bd0af0b41fdd6448bad0e84365ca79cf8923c27b21e46", size = 2731632, upload-time = "2025-05-10T17:34:55.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/c7/47d26d48d779b1b084ebc0d9ec07035167992578768237ef553a3eecc8db/fonttools-4.58.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:25590272f89e94ab5a292d518c549f3a88e6a34fa1193797b7047dfea111b048", size = 2303941, upload-time = "2025-05-10T17:34:58.624Z" }, + { url = "https://files.pythonhosted.org/packages/79/2e/ac80c0fea501f1aa93e2b22d72c97a8c0d14239582b7e8c722185a0540a7/fonttools-4.58.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:614435e9a87abe18bd7bc7ceeb8029e8f181c571317161e89fa3e6e0a4f20f5d", size = 4712776, upload-time = "2025-05-10T17:35:01.124Z" }, + { url = "https://files.pythonhosted.org/packages/f2/5c/b41f9c940dc397ecb41765654efc76e06782bfe0783c3e2affc534be181c/fonttools-4.58.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0154bd86d9a9e880f6e937e4d99c2139a624428dd9852072e12d7a85c79d611e", size = 4743251, upload-time = "2025-05-10T17:35:03.815Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c4/0d3807d922a788b603a3fff622af53e732464b88baf0049a181a90f9b1c6/fonttools-4.58.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5b3660df0b02c9cebbf7baf66952c2fd055e43e658aceb92cc95ba19e0a5c8b6", size = 4795635, upload-time = "2025-05-10T17:35:06.134Z" }, + { url = "https://files.pythonhosted.org/packages/46/74/627bed8e2c7e641c9c572f09970b0980e5513fd29e57b394d4aee2261e30/fonttools-4.58.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c43b7f1d0b818427bb1cd20903d1168271abdcde10eb6247b1995c4e1ed63907", size = 4904720, upload-time = "2025-05-10T17:35:09.015Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f2/7e5d082a98eb61fc0c3055e8a0e061a1eb9fc2d93f0661854bf6cb63c519/fonttools-4.58.0-cp310-cp310-win32.whl", hash = "sha256:5450f40c385cdfa21133245f57b9cf8ce45018a04630a98de61eed8da14b8325", size = 2188180, upload-time = "2025-05-10T17:35:11.494Z" }, + { url = "https://files.pythonhosted.org/packages/00/33/ffd914e3c3a585003d770457188c8eaf7266b7a1cceb6d234ab543a9f958/fonttools-4.58.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0553431696eacafee9aefe94dc3c2bf5d658fbdc7fdba5b341c588f935471c6", size = 2233120, upload-time = "2025-05-10T17:35:13.896Z" }, + { url = "https://files.pythonhosted.org/packages/76/2e/9b9bd943872a50cb182382f8f4a99af92d76e800603d5f73e4343fdce61a/fonttools-4.58.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9345b1bb994476d6034996b31891c0c728c1059c05daa59f9ab57d2a4dce0f84", size = 2751920, upload-time = "2025-05-10T17:35:16.487Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8c/e8d6375da893125f610826c2e30e6d2597dfb8dad256f8ff5a54f3089fda/fonttools-4.58.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1d93119ace1e2d39ff1340deb71097932f72b21c054bd3da727a3859825e24e5", size = 2313957, upload-time = "2025-05-10T17:35:18.906Z" }, + { url = "https://files.pythonhosted.org/packages/4f/1b/a29cb00c8c20164b24f88780e298fafd0bbfb25cf8bc7b10c4b69331ad5d/fonttools-4.58.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79c9e4f01bb04f19df272ae35314eb6349fdb2e9497a163cd22a21be999694bd", size = 4913808, upload-time = "2025-05-10T17:35:21.394Z" }, + { url = "https://files.pythonhosted.org/packages/d1/ab/9b9507b65b15190cbfe1ccd3c08067d79268d8312ef20948b16d9f5aa905/fonttools-4.58.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62ecda1465d38248aaf9bee1c17a21cf0b16aef7d121d7d303dbb320a6fd49c2", size = 4935876, upload-time = "2025-05-10T17:35:23.849Z" }, + { url = "https://files.pythonhosted.org/packages/15/e4/1395853bc775b0ab06a1c61cf261779afda7baff3f65cf1197bbd21aa149/fonttools-4.58.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:29d0499bff12a26733c05c1bfd07e68465158201624b2fba4a40b23d96c43f94", size = 4974798, upload-time = "2025-05-10T17:35:26.189Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b9/0358368ef5462f4653a198207b29885bee8d5e23c870f6125450ed88e693/fonttools-4.58.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1871abdb0af582e2d96cc12d88889e3bfa796928f491ec14d34a2e58ca298c7e", size = 5093560, upload-time = "2025-05-10T17:35:28.577Z" }, + { url = "https://files.pythonhosted.org/packages/11/00/f64bc3659980c41eccf2c371e62eb15b40858f02a41a0e9c6258ef094388/fonttools-4.58.0-cp311-cp311-win32.whl", hash = "sha256:e292485d70402093eb94f6ab7669221743838b8bd4c1f45c84ca76b63338e7bf", size = 2186330, upload-time = "2025-05-10T17:35:31.733Z" }, + { url = "https://files.pythonhosted.org/packages/c8/a0/0287be13a1ec7733abf292ffbd76417cea78752d4ce10fecf92d8b1252d6/fonttools-4.58.0-cp311-cp311-win_amd64.whl", hash = "sha256:6df3755fcf9ad70a74ad3134bd5c9738f73c9bb701a304b1c809877b11fe701c", size = 2234687, upload-time = "2025-05-10T17:35:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/6a/4e/1c6b35ec7c04d739df4cf5aace4b7ec284d6af2533a65de21972e2f237d9/fonttools-4.58.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:aa8316798f982c751d71f0025b372151ea36405733b62d0d94d5e7b8dd674fa6", size = 2737502, upload-time = "2025-05-10T17:35:36.436Z" }, + { url = "https://files.pythonhosted.org/packages/fc/72/c6fcafa3c9ed2b69991ae25a1ba7a3fec8bf74928a96e8229c37faa8eda2/fonttools-4.58.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c6db489511e867633b859b11aefe1b7c0d90281c5bdb903413edbb2ba77b97f1", size = 2307214, upload-time = "2025-05-10T17:35:38.939Z" }, + { url = "https://files.pythonhosted.org/packages/52/11/1015cedc9878da6d8d1758049749eef857b693e5828d477287a959c8650f/fonttools-4.58.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:107bdb2dacb1f627db3c4b77fb16d065a10fe88978d02b4fc327b9ecf8a62060", size = 4811136, upload-time = "2025-05-10T17:35:41.491Z" }, + { url = "https://files.pythonhosted.org/packages/32/b9/6a1bc1af6ec17eead5d32e87075e22d0dab001eace0b5a1542d38c6a9483/fonttools-4.58.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba7212068ab20f1128a0475f169068ba8e5b6e35a39ba1980b9f53f6ac9720ac", size = 4876598, upload-time = "2025-05-10T17:35:43.986Z" }, + { url = "https://files.pythonhosted.org/packages/d8/46/b14584c7ea65ad1609fb9632251016cda8a2cd66b15606753b9f888d3677/fonttools-4.58.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f95ea3b6a3b9962da3c82db73f46d6a6845a6c3f3f968f5293b3ac1864e771c2", size = 4872256, upload-time = "2025-05-10T17:35:46.617Z" }, + { url = "https://files.pythonhosted.org/packages/05/78/b2105a7812ca4ef9bf180cd741c82f4522316c652ce2a56f788e2eb54b62/fonttools-4.58.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:874f1225cc4ccfeac32009887f722d7f8b107ca5e867dcee067597eef9d4c80b", size = 5028710, upload-time = "2025-05-10T17:35:49.227Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a9/a38c85ffd30d1f2c7a5460c8abfd1aa66e00c198df3ff0b08117f5c6fcd9/fonttools-4.58.0-cp312-cp312-win32.whl", hash = "sha256:5f3cde64ec99c43260e2e6c4fa70dfb0a5e2c1c1d27a4f4fe4618c16f6c9ff71", size = 2173593, upload-time = "2025-05-10T17:35:51.226Z" }, + { url = "https://files.pythonhosted.org/packages/66/48/29752962a74b7ed95da976b5a968bba1fe611a4a7e50b9fefa345e6e7025/fonttools-4.58.0-cp312-cp312-win_amd64.whl", hash = "sha256:2aee08e2818de45067109a207cbd1b3072939f77751ef05904d506111df5d824", size = 2223230, upload-time = "2025-05-10T17:35:53.653Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d7/d77cae11c445916d767cace93ba8283b3f360197d95d7470b90a9e984e10/fonttools-4.58.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:4809790f2371d8a08e59e1ce2b734c954cf09742e75642d7f4c46cfdac488fdd", size = 2728320, upload-time = "2025-05-10T17:35:56.455Z" }, + { url = "https://files.pythonhosted.org/packages/77/48/7d8b3c519ef4b48081d40310262224a38785e39a8610ccb92a229a6f085d/fonttools-4.58.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b00f240280f204ce4546b05ff3515bf8ff47a9cae914c718490025ea2bb9b324", size = 2302570, upload-time = "2025-05-10T17:35:58.794Z" }, + { url = "https://files.pythonhosted.org/packages/2c/48/156b83eb8fb7261056e448bfda1b495b90e761b28ec23cee10e3e19f1967/fonttools-4.58.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a62015ad463e1925544e9159dd6eefe33ebfb80938d5ab15d8b1c4b354ff47b", size = 4790066, upload-time = "2025-05-10T17:36:01.174Z" }, + { url = "https://files.pythonhosted.org/packages/60/49/aaecb1b3cea2b9b9c7cea6240d6bc8090feb5489a6fbf93cb68003be979b/fonttools-4.58.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ceef6f6ab58061a811967e3e32e630747fcb823dcc33a9a2c80e2d0d17cb292", size = 4861076, upload-time = "2025-05-10T17:36:03.663Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c8/97cbb41bee81ea9daf6109e0f3f70a274a3c69418e5ac6b0193f5dacf506/fonttools-4.58.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c7be21ac52370b515cdbdd0f400803fd29432a4fa4ddb4244ac8b322e54f36c0", size = 4858394, upload-time = "2025-05-10T17:36:06.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/23/c2c231457361f869a7d7374a557208e303b469d48a4a697c0fb249733ea1/fonttools-4.58.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:85836be4c3c4aacf6fcb7a6f263896d0e9ce431da9fa6fe9213d70f221f131c9", size = 5002160, upload-time = "2025-05-10T17:36:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/a9/e0/c2262f941a43b810c5c192db94b5d1ce8eda91bec2757f7e2416398f4072/fonttools-4.58.0-cp313-cp313-win32.whl", hash = "sha256:2b32b7130277bd742cb8c4379a6a303963597d22adea77a940343f3eadbcaa4c", size = 2171919, upload-time = "2025-05-10T17:36:10.644Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ee/e4aa7bb4ce510ad57a808d321df1bbed1eeb6e1dfb20aaee1a5d9c076849/fonttools-4.58.0-cp313-cp313-win_amd64.whl", hash = "sha256:75e68ee2ec9aaa173cf5e33f243da1d51d653d5e25090f2722bc644a78db0f1a", size = 2222972, upload-time = "2025-05-10T17:36:12.495Z" }, + { url = "https://files.pythonhosted.org/packages/9b/1f/4417c26e26a1feab85a27e927f7a73d8aabc84544be8ba108ce4aa90eb1e/fonttools-4.58.0-py3-none-any.whl", hash = "sha256:c96c36880be2268be409df7b08c5b5dacac1827083461a6bc2cb07b8cbcec1d7", size = 1111440, upload-time = "2025-05-10T17:36:33.607Z" }, +] + +[[package]] +name = "google-auth" +version = "2.40.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "pyasn1-modules" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/a5/38c21d0e731bb716cffcf987bd9a3555cb95877ab4b616cfb96939933f20/google_auth-2.40.1.tar.gz", hash = "sha256:58f0e8416a9814c1d86c9b7f6acf6816b51aba167b2c76821965271bac275540", size = 280975, upload-time = "2025-05-07T01:04:55.3Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/b1/1272c6e80847ba5349f5ccb7574596393d1e222543f5003cb810865c3575/google_auth-2.40.1-py2.py3-none-any.whl", hash = "sha256:ed4cae4f5c46b41bae1d19c036e06f6c371926e97b19e816fc854eff811974ee", size = 216101, upload-time = "2025-05-07T01:04:53.612Z" }, +] + +[[package]] +name = "google-auth-oauthlib" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "requests-oauthlib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/87/e10bf24f7bcffc1421b84d6f9c3377c30ec305d082cd737ddaa6d8f77f7c/google_auth_oauthlib-1.2.2.tar.gz", hash = "sha256:11046fb8d3348b296302dd939ace8af0a724042e8029c1b872d87fabc9f41684", size = 20955, upload-time = "2025-04-22T16:40:29.172Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/84/40ee070be95771acd2f4418981edb834979424565c3eec3cd88b6aa09d24/google_auth_oauthlib-1.2.2-py3-none-any.whl", hash = "sha256:fd619506f4b3908b5df17b65f39ca8d66ea56986e5472eb5978fd8f3786f00a2", size = 19072, upload-time = "2025-04-22T16:40:28.174Z" }, +] + +[[package]] +name = "gspread" +version = "6.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "google-auth-oauthlib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/83/42d1d813822ed016d77aabadc99b09de3b5bd68532fd6bae23fd62347c41/gspread-6.2.1.tar.gz", hash = "sha256:2c7c99f7c32ebea6ec0d36f2d5cbe8a2be5e8f2a48bde87ad1ea203eff32bd03", size = 82590, upload-time = "2025-05-14T15:56:25.254Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/76/563fb20dedd0e12794d9a12cfe0198458cc0501fdc7b034eee2166d035d5/gspread-6.2.1-py3-none-any.whl", hash = "sha256:6d4ec9f1c23ae3c704a9219026dac01f2b328ac70b96f1495055d453c4c184db", size = 59977, upload-time = "2025-05-14T15:56:24.014Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.4.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538, upload-time = "2024-12-24T18:30:51.519Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/5f/4d8e9e852d98ecd26cdf8eaf7ed8bc33174033bba5e07001b289f07308fd/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88c6f252f6816a73b1f8c904f7bbe02fd67c09a69f7cb8a0eecdbf5ce78e63db", size = 124623, upload-time = "2024-12-24T18:28:17.687Z" }, + { url = "https://files.pythonhosted.org/packages/1d/70/7f5af2a18a76fe92ea14675f8bd88ce53ee79e37900fa5f1a1d8e0b42998/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72941acb7b67138f35b879bbe85be0f6c6a70cab78fe3ef6db9c024d9223e5b", size = 66720, upload-time = "2024-12-24T18:28:19.158Z" }, + { url = "https://files.pythonhosted.org/packages/c6/13/e15f804a142353aefd089fadc8f1d985561a15358c97aca27b0979cb0785/kiwisolver-1.4.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce2cf1e5688edcb727fdf7cd1bbd0b6416758996826a8be1d958f91880d0809d", size = 65413, upload-time = "2024-12-24T18:28:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/ce/6d/67d36c4d2054e83fb875c6b59d0809d5c530de8148846b1370475eeeece9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c8bf637892dc6e6aad2bc6d4d69d08764166e5e3f69d469e55427b6ac001b19d", size = 1650826, upload-time = "2024-12-24T18:28:21.203Z" }, + { url = "https://files.pythonhosted.org/packages/de/c6/7b9bb8044e150d4d1558423a1568e4f227193662a02231064e3824f37e0a/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:034d2c891f76bd3edbdb3ea11140d8510dca675443da7304205a2eaa45d8334c", size = 1628231, upload-time = "2024-12-24T18:28:23.851Z" }, + { url = "https://files.pythonhosted.org/packages/b6/38/ad10d437563063eaaedbe2c3540a71101fc7fb07a7e71f855e93ea4de605/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47b28d1dfe0793d5e96bce90835e17edf9a499b53969b03c6c47ea5985844c3", size = 1408938, upload-time = "2024-12-24T18:28:26.687Z" }, + { url = "https://files.pythonhosted.org/packages/52/ce/c0106b3bd7f9e665c5f5bc1e07cc95b5dabd4e08e3dad42dbe2faad467e7/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb158fe28ca0c29f2260cca8c43005329ad58452c36f0edf298204de32a9a3ed", size = 1422799, upload-time = "2024-12-24T18:28:30.538Z" }, + { url = "https://files.pythonhosted.org/packages/d0/87/efb704b1d75dc9758087ba374c0f23d3254505edaedd09cf9d247f7878b9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5536185fce131780ebd809f8e623bf4030ce1b161353166c49a3c74c287897f", size = 1354362, upload-time = "2024-12-24T18:28:32.943Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b3/fd760dc214ec9a8f208b99e42e8f0130ff4b384eca8b29dd0efc62052176/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:369b75d40abedc1da2c1f4de13f3482cb99e3237b38726710f4a793432b1c5ff", size = 2222695, upload-time = "2024-12-24T18:28:35.641Z" }, + { url = "https://files.pythonhosted.org/packages/a2/09/a27fb36cca3fc01700687cc45dae7a6a5f8eeb5f657b9f710f788748e10d/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:641f2ddf9358c80faa22e22eb4c9f54bd3f0e442e038728f500e3b978d00aa7d", size = 2370802, upload-time = "2024-12-24T18:28:38.357Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c3/ba0a0346db35fe4dc1f2f2cf8b99362fbb922d7562e5f911f7ce7a7b60fa/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d561d2d8883e0819445cfe58d7ddd673e4015c3c57261d7bdcd3710d0d14005c", size = 2334646, upload-time = "2024-12-24T18:28:40.941Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/942cf69e562f5ed253ac67d5c92a693745f0bed3c81f49fc0cbebe4d6b00/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1732e065704b47c9afca7ffa272f845300a4eb959276bf6970dc07265e73b605", size = 2467260, upload-time = "2024-12-24T18:28:42.273Z" }, + { url = "https://files.pythonhosted.org/packages/32/26/2d9668f30d8a494b0411d4d7d4ea1345ba12deb6a75274d58dd6ea01e951/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bcb1ebc3547619c3b58a39e2448af089ea2ef44b37988caf432447374941574e", size = 2288633, upload-time = "2024-12-24T18:28:44.87Z" }, + { url = "https://files.pythonhosted.org/packages/98/99/0dd05071654aa44fe5d5e350729961e7bb535372935a45ac89a8924316e6/kiwisolver-1.4.8-cp310-cp310-win_amd64.whl", hash = "sha256:89c107041f7b27844179ea9c85d6da275aa55ecf28413e87624d033cf1f6b751", size = 71885, upload-time = "2024-12-24T18:28:47.346Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fc/822e532262a97442989335394d441cd1d0448c2e46d26d3e04efca84df22/kiwisolver-1.4.8-cp310-cp310-win_arm64.whl", hash = "sha256:b5773efa2be9eb9fcf5415ea3ab70fc785d598729fd6057bea38d539ead28271", size = 65175, upload-time = "2024-12-24T18:28:49.651Z" }, + { url = "https://files.pythonhosted.org/packages/da/ed/c913ee28936c371418cb167b128066ffb20bbf37771eecc2c97edf8a6e4c/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a4d3601908c560bdf880f07d94f31d734afd1bb71e96585cace0e38ef44c6d84", size = 124635, upload-time = "2024-12-24T18:28:51.826Z" }, + { url = "https://files.pythonhosted.org/packages/4c/45/4a7f896f7467aaf5f56ef093d1f329346f3b594e77c6a3c327b2d415f521/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856b269c4d28a5c0d5e6c1955ec36ebfd1651ac00e1ce0afa3e28da95293b561", size = 66717, upload-time = "2024-12-24T18:28:54.256Z" }, + { url = "https://files.pythonhosted.org/packages/5f/b4/c12b3ac0852a3a68f94598d4c8d569f55361beef6159dce4e7b624160da2/kiwisolver-1.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c2b9a96e0f326205af81a15718a9073328df1173a2619a68553decb7097fd5d7", size = 65413, upload-time = "2024-12-24T18:28:55.184Z" }, + { url = "https://files.pythonhosted.org/packages/a9/98/1df4089b1ed23d83d410adfdc5947245c753bddfbe06541c4aae330e9e70/kiwisolver-1.4.8-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5020c83e8553f770cb3b5fc13faac40f17e0b205bd237aebd21d53d733adb03", size = 1343994, upload-time = "2024-12-24T18:28:57.493Z" }, + { url = "https://files.pythonhosted.org/packages/8d/bf/b4b169b050c8421a7c53ea1ea74e4ef9c335ee9013216c558a047f162d20/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dace81d28c787956bfbfbbfd72fdcef014f37d9b48830829e488fdb32b49d954", size = 1434804, upload-time = "2024-12-24T18:29:00.077Z" }, + { url = "https://files.pythonhosted.org/packages/66/5a/e13bd341fbcf73325ea60fdc8af752addf75c5079867af2e04cc41f34434/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11e1022b524bd48ae56c9b4f9296bce77e15a2e42a502cceba602f804b32bb79", size = 1450690, upload-time = "2024-12-24T18:29:01.401Z" }, + { url = "https://files.pythonhosted.org/packages/9b/4f/5955dcb376ba4a830384cc6fab7d7547bd6759fe75a09564910e9e3bb8ea/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b9b4d2892fefc886f30301cdd80debd8bb01ecdf165a449eb6e78f79f0fabd6", size = 1376839, upload-time = "2024-12-24T18:29:02.685Z" }, + { url = "https://files.pythonhosted.org/packages/3a/97/5edbed69a9d0caa2e4aa616ae7df8127e10f6586940aa683a496c2c280b9/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a96c0e790ee875d65e340ab383700e2b4891677b7fcd30a699146f9384a2bb0", size = 1435109, upload-time = "2024-12-24T18:29:04.113Z" }, + { url = "https://files.pythonhosted.org/packages/13/fc/e756382cb64e556af6c1809a1bbb22c141bbc2445049f2da06b420fe52bf/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23454ff084b07ac54ca8be535f4174170c1094a4cff78fbae4f73a4bcc0d4dab", size = 2245269, upload-time = "2024-12-24T18:29:05.488Z" }, + { url = "https://files.pythonhosted.org/packages/76/15/e59e45829d7f41c776d138245cabae6515cb4eb44b418f6d4109c478b481/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:87b287251ad6488e95b4f0b4a79a6d04d3ea35fde6340eb38fbd1ca9cd35bbbc", size = 2393468, upload-time = "2024-12-24T18:29:06.79Z" }, + { url = "https://files.pythonhosted.org/packages/e9/39/483558c2a913ab8384d6e4b66a932406f87c95a6080112433da5ed668559/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b21dbe165081142b1232a240fc6383fd32cdd877ca6cc89eab93e5f5883e1c25", size = 2355394, upload-time = "2024-12-24T18:29:08.24Z" }, + { url = "https://files.pythonhosted.org/packages/01/aa/efad1fbca6570a161d29224f14b082960c7e08268a133fe5dc0f6906820e/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:768cade2c2df13db52475bd28d3a3fac8c9eff04b0e9e2fda0f3760f20b3f7fc", size = 2490901, upload-time = "2024-12-24T18:29:09.653Z" }, + { url = "https://files.pythonhosted.org/packages/c9/4f/15988966ba46bcd5ab9d0c8296914436720dd67fca689ae1a75b4ec1c72f/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d47cfb2650f0e103d4bf68b0b5804c68da97272c84bb12850d877a95c056bd67", size = 2312306, upload-time = "2024-12-24T18:29:12.644Z" }, + { url = "https://files.pythonhosted.org/packages/2d/27/bdf1c769c83f74d98cbc34483a972f221440703054894a37d174fba8aa68/kiwisolver-1.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:ed33ca2002a779a2e20eeb06aea7721b6e47f2d4b8a8ece979d8ba9e2a167e34", size = 71966, upload-time = "2024-12-24T18:29:14.089Z" }, + { url = "https://files.pythonhosted.org/packages/4a/c9/9642ea855604aeb2968a8e145fc662edf61db7632ad2e4fb92424be6b6c0/kiwisolver-1.4.8-cp311-cp311-win_arm64.whl", hash = "sha256:16523b40aab60426ffdebe33ac374457cf62863e330a90a0383639ce14bf44b2", size = 65311, upload-time = "2024-12-24T18:29:15.892Z" }, + { url = "https://files.pythonhosted.org/packages/fc/aa/cea685c4ab647f349c3bc92d2daf7ae34c8e8cf405a6dcd3a497f58a2ac3/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6af5e8815fd02997cb6ad9bbed0ee1e60014438ee1a5c2444c96f87b8843502", size = 124152, upload-time = "2024-12-24T18:29:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/c5/0b/8db6d2e2452d60d5ebc4ce4b204feeb16176a851fd42462f66ade6808084/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bade438f86e21d91e0cf5dd7c0ed00cda0f77c8c1616bd83f9fc157fa6760d31", size = 66555, upload-time = "2024-12-24T18:29:19.146Z" }, + { url = "https://files.pythonhosted.org/packages/60/26/d6a0db6785dd35d3ba5bf2b2df0aedc5af089962c6eb2cbf67a15b81369e/kiwisolver-1.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b83dc6769ddbc57613280118fb4ce3cd08899cc3369f7d0e0fab518a7cf37fdb", size = 65067, upload-time = "2024-12-24T18:29:20.096Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ed/1d97f7e3561e09757a196231edccc1bcf59d55ddccefa2afc9c615abd8e0/kiwisolver-1.4.8-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111793b232842991be367ed828076b03d96202c19221b5ebab421ce8bcad016f", size = 1378443, upload-time = "2024-12-24T18:29:22.843Z" }, + { url = "https://files.pythonhosted.org/packages/29/61/39d30b99954e6b46f760e6289c12fede2ab96a254c443639052d1b573fbc/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:257af1622860e51b1a9d0ce387bf5c2c4f36a90594cb9514f55b074bcc787cfc", size = 1472728, upload-time = "2024-12-24T18:29:24.463Z" }, + { url = "https://files.pythonhosted.org/packages/0c/3e/804163b932f7603ef256e4a715e5843a9600802bb23a68b4e08c8c0ff61d/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b5637c3f316cab1ec1c9a12b8c5f4750a4c4b71af9157645bf32830e39c03a", size = 1478388, upload-time = "2024-12-24T18:29:25.776Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9e/60eaa75169a154700be74f875a4d9961b11ba048bef315fbe89cb6999056/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:782bb86f245ec18009890e7cb8d13a5ef54dcf2ebe18ed65f795e635a96a1c6a", size = 1413849, upload-time = "2024-12-24T18:29:27.202Z" }, + { url = "https://files.pythonhosted.org/packages/bc/b3/9458adb9472e61a998c8c4d95cfdfec91c73c53a375b30b1428310f923e4/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc978a80a0db3a66d25767b03688f1147a69e6237175c0f4ffffaaedf744055a", size = 1475533, upload-time = "2024-12-24T18:29:28.638Z" }, + { url = "https://files.pythonhosted.org/packages/e4/7a/0a42d9571e35798de80aef4bb43a9b672aa7f8e58643d7bd1950398ffb0a/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:36dbbfd34838500a31f52c9786990d00150860e46cd5041386f217101350f0d3", size = 2268898, upload-time = "2024-12-24T18:29:30.368Z" }, + { url = "https://files.pythonhosted.org/packages/d9/07/1255dc8d80271400126ed8db35a1795b1a2c098ac3a72645075d06fe5c5d/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:eaa973f1e05131de5ff3569bbba7f5fd07ea0595d3870ed4a526d486fe57fa1b", size = 2425605, upload-time = "2024-12-24T18:29:33.151Z" }, + { url = "https://files.pythonhosted.org/packages/84/df/5a3b4cf13780ef6f6942df67b138b03b7e79e9f1f08f57c49957d5867f6e/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a66f60f8d0c87ab7f59b6fb80e642ebb29fec354a4dfad687ca4092ae69d04f4", size = 2375801, upload-time = "2024-12-24T18:29:34.584Z" }, + { url = "https://files.pythonhosted.org/packages/8f/10/2348d068e8b0f635c8c86892788dac7a6b5c0cb12356620ab575775aad89/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858416b7fb777a53f0c59ca08190ce24e9abbd3cffa18886a5781b8e3e26f65d", size = 2520077, upload-time = "2024-12-24T18:29:36.138Z" }, + { url = "https://files.pythonhosted.org/packages/32/d8/014b89fee5d4dce157d814303b0fce4d31385a2af4c41fed194b173b81ac/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:085940635c62697391baafaaeabdf3dd7a6c3643577dde337f4d66eba021b2b8", size = 2338410, upload-time = "2024-12-24T18:29:39.991Z" }, + { url = "https://files.pythonhosted.org/packages/bd/72/dfff0cc97f2a0776e1c9eb5bef1ddfd45f46246c6533b0191887a427bca5/kiwisolver-1.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:01c3d31902c7db5fb6182832713d3b4122ad9317c2c5877d0539227d96bb2e50", size = 71853, upload-time = "2024-12-24T18:29:42.006Z" }, + { url = "https://files.pythonhosted.org/packages/dc/85/220d13d914485c0948a00f0b9eb419efaf6da81b7d72e88ce2391f7aed8d/kiwisolver-1.4.8-cp312-cp312-win_arm64.whl", hash = "sha256:a3c44cb68861de93f0c4a8175fbaa691f0aa22550c331fefef02b618a9dcb476", size = 65424, upload-time = "2024-12-24T18:29:44.38Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/e62464a652f4f8cd9006e13d07abad844a47df1e6537f73ddfbf1bc997ec/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1c8ceb754339793c24aee1c9fb2485b5b1f5bb1c2c214ff13368431e51fc9a09", size = 124156, upload-time = "2024-12-24T18:29:45.368Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2d/f13d06998b546a2ad4f48607a146e045bbe48030774de29f90bdc573df15/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a62808ac74b5e55a04a408cda6156f986cefbcf0ada13572696b507cc92fa1", size = 66555, upload-time = "2024-12-24T18:29:46.37Z" }, + { url = "https://files.pythonhosted.org/packages/59/e3/b8bd14b0a54998a9fd1e8da591c60998dc003618cb19a3f94cb233ec1511/kiwisolver-1.4.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68269e60ee4929893aad82666821aaacbd455284124817af45c11e50a4b42e3c", size = 65071, upload-time = "2024-12-24T18:29:47.333Z" }, + { url = "https://files.pythonhosted.org/packages/f0/1c/6c86f6d85ffe4d0ce04228d976f00674f1df5dc893bf2dd4f1928748f187/kiwisolver-1.4.8-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34d142fba9c464bc3bbfeff15c96eab0e7310343d6aefb62a79d51421fcc5f1b", size = 1378053, upload-time = "2024-12-24T18:29:49.636Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b9/1c6e9f6dcb103ac5cf87cb695845f5fa71379021500153566d8a8a9fc291/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc373e0eef45b59197de815b1b28ef89ae3955e7722cc9710fb91cd77b7f47", size = 1472278, upload-time = "2024-12-24T18:29:51.164Z" }, + { url = "https://files.pythonhosted.org/packages/ee/81/aca1eb176de671f8bda479b11acdc42c132b61a2ac861c883907dde6debb/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77e6f57a20b9bd4e1e2cedda4d0b986ebd0216236f0106e55c28aea3d3d69b16", size = 1478139, upload-time = "2024-12-24T18:29:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/49/f4/e081522473671c97b2687d380e9e4c26f748a86363ce5af48b4a28e48d06/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08e77738ed7538f036cd1170cbed942ef749137b1311fa2bbe2a7fda2f6bf3cc", size = 1413517, upload-time = "2024-12-24T18:29:53.941Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e9/6a7d025d8da8c4931522922cd706105aa32b3291d1add8c5427cdcd66e63/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5ce1e481a74b44dd5e92ff03ea0cb371ae7a0268318e202be06c8f04f4f1246", size = 1474952, upload-time = "2024-12-24T18:29:56.523Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/13fa685ae167bee5d94b415991c4fc7bb0a1b6ebea6e753a87044b209678/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc2ace710ba7c1dfd1a3b42530b62b9ceed115f19a1656adefce7b1782a37794", size = 2269132, upload-time = "2024-12-24T18:29:57.989Z" }, + { url = "https://files.pythonhosted.org/packages/ef/92/bb7c9395489b99a6cb41d502d3686bac692586db2045adc19e45ee64ed23/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3452046c37c7692bd52b0e752b87954ef86ee2224e624ef7ce6cb21e8c41cc1b", size = 2425997, upload-time = "2024-12-24T18:29:59.393Z" }, + { url = "https://files.pythonhosted.org/packages/ed/12/87f0e9271e2b63d35d0d8524954145837dd1a6c15b62a2d8c1ebe0f182b4/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e9a60b50fe8b2ec6f448fe8d81b07e40141bfced7f896309df271a0b92f80f3", size = 2376060, upload-time = "2024-12-24T18:30:01.338Z" }, + { url = "https://files.pythonhosted.org/packages/02/6e/c8af39288edbce8bf0fa35dee427b082758a4b71e9c91ef18fa667782138/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:918139571133f366e8362fa4a297aeba86c7816b7ecf0bc79168080e2bd79957", size = 2520471, upload-time = "2024-12-24T18:30:04.574Z" }, + { url = "https://files.pythonhosted.org/packages/13/78/df381bc7b26e535c91469f77f16adcd073beb3e2dd25042efd064af82323/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e063ef9f89885a1d68dd8b2e18f5ead48653176d10a0e324e3b0030e3a69adeb", size = 2338793, upload-time = "2024-12-24T18:30:06.25Z" }, + { url = "https://files.pythonhosted.org/packages/d0/dc/c1abe38c37c071d0fc71c9a474fd0b9ede05d42f5a458d584619cfd2371a/kiwisolver-1.4.8-cp313-cp313-win_amd64.whl", hash = "sha256:a17b7c4f5b2c51bb68ed379defd608a03954a1845dfed7cc0117f1cc8a9b7fd2", size = 71855, upload-time = "2024-12-24T18:30:07.535Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b6/21529d595b126ac298fdd90b705d87d4c5693de60023e0efcb4f387ed99e/kiwisolver-1.4.8-cp313-cp313-win_arm64.whl", hash = "sha256:3cd3bc628b25f74aedc6d374d5babf0166a92ff1317f46267f12d2ed54bc1d30", size = 65430, upload-time = "2024-12-24T18:30:08.504Z" }, + { url = "https://files.pythonhosted.org/packages/34/bd/b89380b7298e3af9b39f49334e3e2a4af0e04819789f04b43d560516c0c8/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:370fd2df41660ed4e26b8c9d6bbcad668fbe2560462cba151a721d49e5b6628c", size = 126294, upload-time = "2024-12-24T18:30:09.508Z" }, + { url = "https://files.pythonhosted.org/packages/83/41/5857dc72e5e4148eaac5aa76e0703e594e4465f8ab7ec0fc60e3a9bb8fea/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:84a2f830d42707de1d191b9490ac186bf7997a9495d4e9072210a1296345f7dc", size = 67736, upload-time = "2024-12-24T18:30:11.039Z" }, + { url = "https://files.pythonhosted.org/packages/e1/d1/be059b8db56ac270489fb0b3297fd1e53d195ba76e9bbb30e5401fa6b759/kiwisolver-1.4.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7a3ad337add5148cf51ce0b55642dc551c0b9d6248458a757f98796ca7348712", size = 66194, upload-time = "2024-12-24T18:30:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e1/83/4b73975f149819eb7dcf9299ed467eba068ecb16439a98990dcb12e63fdd/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7506488470f41169b86d8c9aeff587293f530a23a23a49d6bc64dab66bedc71e", size = 1465942, upload-time = "2024-12-24T18:30:18.927Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2c/30a5cdde5102958e602c07466bce058b9d7cb48734aa7a4327261ac8e002/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f0121b07b356a22fb0414cec4666bbe36fd6d0d759db3d37228f496ed67c880", size = 1595341, upload-time = "2024-12-24T18:30:22.102Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9b/1e71db1c000385aa069704f5990574b8244cce854ecd83119c19e83c9586/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6d6bd87df62c27d4185de7c511c6248040afae67028a8a22012b010bc7ad062", size = 1598455, upload-time = "2024-12-24T18:30:24.947Z" }, + { url = "https://files.pythonhosted.org/packages/85/92/c8fec52ddf06231b31cbb779af77e99b8253cd96bd135250b9498144c78b/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:291331973c64bb9cce50bbe871fb2e675c4331dab4f31abe89f175ad7679a4d7", size = 1522138, upload-time = "2024-12-24T18:30:26.286Z" }, + { url = "https://files.pythonhosted.org/packages/0b/51/9eb7e2cd07a15d8bdd976f6190c0164f92ce1904e5c0c79198c4972926b7/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:893f5525bb92d3d735878ec00f781b2de998333659507d29ea4466208df37bed", size = 1582857, upload-time = "2024-12-24T18:30:28.86Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/c5a00387a5405e68ba32cc64af65ce881a39b98d73cc394b24143bebc5b8/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b47a465040146981dc9db8647981b8cb96366fbc8d452b031e4f8fdffec3f26d", size = 2293129, upload-time = "2024-12-24T18:30:30.34Z" }, + { url = "https://files.pythonhosted.org/packages/44/83/eeb7af7d706b8347548313fa3a3a15931f404533cc54fe01f39e830dd231/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:99cea8b9dd34ff80c521aef46a1dddb0dcc0283cf18bde6d756f1e6f31772165", size = 2421538, upload-time = "2024-12-24T18:30:33.334Z" }, + { url = "https://files.pythonhosted.org/packages/05/f9/27e94c1b3eb29e6933b6986ffc5fa1177d2cd1f0c8efc5f02c91c9ac61de/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:151dffc4865e5fe6dafce5480fab84f950d14566c480c08a53c663a0020504b6", size = 2390661, upload-time = "2024-12-24T18:30:34.939Z" }, + { url = "https://files.pythonhosted.org/packages/d9/d4/3c9735faa36ac591a4afcc2980d2691000506050b7a7e80bcfe44048daa7/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:577facaa411c10421314598b50413aa1ebcf5126f704f1e5d72d7e4e9f020d90", size = 2546710, upload-time = "2024-12-24T18:30:37.281Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fa/be89a49c640930180657482a74970cdcf6f7072c8d2471e1babe17a222dc/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85", size = 2349213, upload-time = "2024-12-24T18:30:40.019Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f9/ae81c47a43e33b93b0a9819cac6723257f5da2a5a60daf46aa5c7226ea85/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e7a019419b7b510f0f7c9dceff8c5eae2392037eae483a7f9162625233802b0a", size = 60403, upload-time = "2024-12-24T18:30:41.372Z" }, + { url = "https://files.pythonhosted.org/packages/58/ca/f92b5cb6f4ce0c1ebfcfe3e2e42b96917e16f7090e45b21102941924f18f/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:286b18e86682fd2217a48fc6be6b0f20c1d0ed10958d8dc53453ad58d7be0bf8", size = 58657, upload-time = "2024-12-24T18:30:42.392Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/ae0240f732f0484d3a4dc885d055653c47144bdf59b670aae0ec3c65a7c8/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4191ee8dfd0be1c3666ccbac178c5a05d5f8d689bbe3fc92f3c4abec817f8fe0", size = 84948, upload-time = "2024-12-24T18:30:44.703Z" }, + { url = "https://files.pythonhosted.org/packages/5d/eb/78d50346c51db22c7203c1611f9b513075f35c4e0e4877c5dde378d66043/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd2785b9391f2873ad46088ed7599a6a71e762e1ea33e87514b1a441ed1da1c", size = 81186, upload-time = "2024-12-24T18:30:45.654Z" }, + { url = "https://files.pythonhosted.org/packages/43/f8/7259f18c77adca88d5f64f9a522792e178b2691f3748817a8750c2d216ef/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c07b29089b7ba090b6f1a669f1411f27221c3662b3a1b7010e67b59bb5a6f10b", size = 80279, upload-time = "2024-12-24T18:30:47.951Z" }, + { url = "https://files.pythonhosted.org/packages/3a/1d/50ad811d1c5dae091e4cf046beba925bcae0a610e79ae4c538f996f63ed5/kiwisolver-1.4.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:65ea09a5a3faadd59c2ce96dc7bf0f364986a315949dc6374f04396b0d60e09b", size = 71762, upload-time = "2024-12-24T18:30:48.903Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/91/d49359a21893183ed2a5b6c76bec40e0b1dcbf8ca148f864d134897cfc75/matplotlib-3.10.3.tar.gz", hash = "sha256:2f82d2c5bb7ae93aaaa4cd42aca65d76ce6376f83304fa3a630b569aca274df0", size = 34799811, upload-time = "2025-05-08T19:10:54.39Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/ea/2bba25d289d389c7451f331ecd593944b3705f06ddf593fa7be75037d308/matplotlib-3.10.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:213fadd6348d106ca7db99e113f1bea1e65e383c3ba76e8556ba4a3054b65ae7", size = 8167862, upload-time = "2025-05-08T19:09:39.563Z" }, + { url = "https://files.pythonhosted.org/packages/41/81/cc70b5138c926604e8c9ed810ed4c79e8116ba72e02230852f5c12c87ba2/matplotlib-3.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3bec61cb8221f0ca6313889308326e7bb303d0d302c5cc9e523b2f2e6c73deb", size = 8042149, upload-time = "2025-05-08T19:09:42.413Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9a/0ff45b6bfa42bb16de597e6058edf2361c298ad5ef93b327728145161bbf/matplotlib-3.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c21ae75651c0231b3ba014b6d5e08fb969c40cdb5a011e33e99ed0c9ea86ecb", size = 8453719, upload-time = "2025-05-08T19:09:44.901Z" }, + { url = "https://files.pythonhosted.org/packages/85/c7/1866e972fed6d71ef136efbc980d4d1854ab7ef1ea8152bbd995ca231c81/matplotlib-3.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a49e39755580b08e30e3620efc659330eac5d6534ab7eae50fa5e31f53ee4e30", size = 8590801, upload-time = "2025-05-08T19:09:47.404Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b9/748f6626d534ab7e255bdc39dc22634d337cf3ce200f261b5d65742044a1/matplotlib-3.10.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cf4636203e1190871d3a73664dea03d26fb019b66692cbfd642faafdad6208e8", size = 9402111, upload-time = "2025-05-08T19:09:49.474Z" }, + { url = "https://files.pythonhosted.org/packages/1f/78/8bf07bd8fb67ea5665a6af188e70b57fcb2ab67057daa06b85a08e59160a/matplotlib-3.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:fd5641a9bb9d55f4dd2afe897a53b537c834b9012684c8444cc105895c8c16fd", size = 8057213, upload-time = "2025-05-08T19:09:51.489Z" }, + { url = "https://files.pythonhosted.org/packages/f5/bd/af9f655456f60fe1d575f54fb14704ee299b16e999704817a7645dfce6b0/matplotlib-3.10.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0ef061f74cd488586f552d0c336b2f078d43bc00dc473d2c3e7bfee2272f3fa8", size = 8178873, upload-time = "2025-05-08T19:09:53.857Z" }, + { url = "https://files.pythonhosted.org/packages/c2/86/e1c86690610661cd716eda5f9d0b35eaf606ae6c9b6736687cfc8f2d0cd8/matplotlib-3.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d96985d14dc5f4a736bbea4b9de9afaa735f8a0fc2ca75be2fa9e96b2097369d", size = 8052205, upload-time = "2025-05-08T19:09:55.684Z" }, + { url = "https://files.pythonhosted.org/packages/54/51/a9f8e49af3883dacddb2da1af5fca1f7468677f1188936452dd9aaaeb9ed/matplotlib-3.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c5f0283da91e9522bdba4d6583ed9d5521566f63729ffb68334f86d0bb98049", size = 8465823, upload-time = "2025-05-08T19:09:57.442Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e3/c82963a3b86d6e6d5874cbeaa390166458a7f1961bab9feb14d3d1a10f02/matplotlib-3.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdfa07c0ec58035242bc8b2c8aae37037c9a886370eef6850703d7583e19964b", size = 8606464, upload-time = "2025-05-08T19:09:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/0e/34/24da1027e7fcdd9e82da3194c470143c551852757a4b473a09a012f5b945/matplotlib-3.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c0b9849a17bce080a16ebcb80a7b714b5677d0ec32161a2cc0a8e5a6030ae220", size = 9413103, upload-time = "2025-05-08T19:10:03.208Z" }, + { url = "https://files.pythonhosted.org/packages/a6/da/948a017c3ea13fd4a97afad5fdebe2f5bbc4d28c0654510ce6fd6b06b7bd/matplotlib-3.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:eef6ed6c03717083bc6d69c2d7ee8624205c29a8e6ea5a31cd3492ecdbaee1e1", size = 8065492, upload-time = "2025-05-08T19:10:05.271Z" }, + { url = "https://files.pythonhosted.org/packages/eb/43/6b80eb47d1071f234ef0c96ca370c2ca621f91c12045f1401b5c9b28a639/matplotlib-3.10.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ab1affc11d1f495ab9e6362b8174a25afc19c081ba5b0775ef00533a4236eea", size = 8179689, upload-time = "2025-05-08T19:10:07.602Z" }, + { url = "https://files.pythonhosted.org/packages/0f/70/d61a591958325c357204870b5e7b164f93f2a8cca1dc6ce940f563909a13/matplotlib-3.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2a818d8bdcafa7ed2eed74487fdb071c09c1ae24152d403952adad11fa3c65b4", size = 8050466, upload-time = "2025-05-08T19:10:09.383Z" }, + { url = "https://files.pythonhosted.org/packages/e7/75/70c9d2306203148cc7902a961240c5927dd8728afedf35e6a77e105a2985/matplotlib-3.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748ebc3470c253e770b17d8b0557f0aa85cf8c63fd52f1a61af5b27ec0b7ffee", size = 8456252, upload-time = "2025-05-08T19:10:11.958Z" }, + { url = "https://files.pythonhosted.org/packages/c4/91/ba0ae1ff4b3f30972ad01cd4a8029e70a0ec3b8ea5be04764b128b66f763/matplotlib-3.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed70453fd99733293ace1aec568255bc51c6361cb0da94fa5ebf0649fdb2150a", size = 8601321, upload-time = "2025-05-08T19:10:14.47Z" }, + { url = "https://files.pythonhosted.org/packages/d2/88/d636041eb54a84b889e11872d91f7cbf036b3b0e194a70fa064eb8b04f7a/matplotlib-3.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dbed9917b44070e55640bd13419de83b4c918e52d97561544814ba463811cbc7", size = 9406972, upload-time = "2025-05-08T19:10:16.569Z" }, + { url = "https://files.pythonhosted.org/packages/b1/79/0d1c165eac44405a86478082e225fce87874f7198300bbebc55faaf6d28d/matplotlib-3.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:cf37d8c6ef1a48829443e8ba5227b44236d7fcaf7647caa3178a4ff9f7a5be05", size = 8067954, upload-time = "2025-05-08T19:10:18.663Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c1/23cfb566a74c696a3b338d8955c549900d18fe2b898b6e94d682ca21e7c2/matplotlib-3.10.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9f2efccc8dcf2b86fc4ee849eea5dcaecedd0773b30f47980dc0cbeabf26ec84", size = 8180318, upload-time = "2025-05-08T19:10:20.426Z" }, + { url = "https://files.pythonhosted.org/packages/6c/0c/02f1c3b66b30da9ee343c343acbb6251bef5b01d34fad732446eaadcd108/matplotlib-3.10.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ddbba06a6c126e3301c3d272a99dcbe7f6c24c14024e80307ff03791a5f294e", size = 8051132, upload-time = "2025-05-08T19:10:22.569Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ab/8db1a5ac9b3a7352fb914133001dae889f9fcecb3146541be46bed41339c/matplotlib-3.10.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748302b33ae9326995b238f606e9ed840bf5886ebafcb233775d946aa8107a15", size = 8457633, upload-time = "2025-05-08T19:10:24.749Z" }, + { url = "https://files.pythonhosted.org/packages/f5/64/41c4367bcaecbc03ef0d2a3ecee58a7065d0a36ae1aa817fe573a2da66d4/matplotlib-3.10.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a80fcccbef63302c0efd78042ea3c2436104c5b1a4d3ae20f864593696364ac7", size = 8601031, upload-time = "2025-05-08T19:10:27.03Z" }, + { url = "https://files.pythonhosted.org/packages/12/6f/6cc79e9e5ab89d13ed64da28898e40fe5b105a9ab9c98f83abd24e46d7d7/matplotlib-3.10.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:55e46cbfe1f8586adb34f7587c3e4f7dedc59d5226719faf6cb54fc24f2fd52d", size = 9406988, upload-time = "2025-05-08T19:10:29.056Z" }, + { url = "https://files.pythonhosted.org/packages/b1/0f/eed564407bd4d935ffabf561ed31099ed609e19287409a27b6d336848653/matplotlib-3.10.3-cp313-cp313-win_amd64.whl", hash = "sha256:151d89cb8d33cb23345cd12490c76fd5d18a56581a16d950b48c6ff19bb2ab93", size = 8068034, upload-time = "2025-05-08T19:10:31.221Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e5/2f14791ff69b12b09e9975e1d116d9578ac684460860ce542c2588cb7a1c/matplotlib-3.10.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c26dd9834e74d164d06433dc7be5d75a1e9890b926b3e57e74fa446e1a62c3e2", size = 8218223, upload-time = "2025-05-08T19:10:33.114Z" }, + { url = "https://files.pythonhosted.org/packages/5c/08/30a94afd828b6e02d0a52cae4a29d6e9ccfcf4c8b56cc28b021d3588873e/matplotlib-3.10.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:24853dad5b8c84c8c2390fc31ce4858b6df504156893292ce8092d190ef8151d", size = 8094985, upload-time = "2025-05-08T19:10:35.337Z" }, + { url = "https://files.pythonhosted.org/packages/89/44/f3bc6b53066c889d7a1a3ea8094c13af6a667c5ca6220ec60ecceec2dabe/matplotlib-3.10.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68f7878214d369d7d4215e2a9075fef743be38fa401d32e6020bab2dfabaa566", size = 8483109, upload-time = "2025-05-08T19:10:37.611Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c7/473bc559beec08ebee9f86ca77a844b65747e1a6c2691e8c92e40b9f42a8/matplotlib-3.10.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6929fc618cb6db9cb75086f73b3219bbb25920cb24cee2ea7a12b04971a4158", size = 8618082, upload-time = "2025-05-08T19:10:39.892Z" }, + { url = "https://files.pythonhosted.org/packages/d8/e9/6ce8edd264c8819e37bbed8172e0ccdc7107fe86999b76ab5752276357a4/matplotlib-3.10.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c7818292a5cc372a2dc4c795e5c356942eb8350b98ef913f7fda51fe175ac5d", size = 9413699, upload-time = "2025-05-08T19:10:42.376Z" }, + { url = "https://files.pythonhosted.org/packages/1b/92/9a45c91089c3cf690b5badd4be81e392ff086ccca8a1d4e3a08463d8a966/matplotlib-3.10.3-cp313-cp313t-win_amd64.whl", hash = "sha256:4f23ffe95c5667ef8a2b56eea9b53db7f43910fa4a2d5472ae0f72b64deab4d5", size = 8139044, upload-time = "2025-05-08T19:10:44.551Z" }, + { url = "https://files.pythonhosted.org/packages/3d/d1/f54d43e95384b312ffa4a74a4326c722f3b8187aaaa12e9a84cdf3037131/matplotlib-3.10.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:86ab63d66bbc83fdb6733471d3bff40897c1e9921cba112accd748eee4bce5e4", size = 8162896, upload-time = "2025-05-08T19:10:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/24/a4/fbfc00c2346177c95b353dcf9b5a004106abe8730a62cb6f27e79df0a698/matplotlib-3.10.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a48f9c08bf7444b5d2391a83e75edb464ccda3c380384b36532a0962593a1751", size = 8039702, upload-time = "2025-05-08T19:10:49.634Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b9/59e120d24a2ec5fc2d30646adb2efb4621aab3c6d83d66fb2a7a182db032/matplotlib-3.10.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb73d8aa75a237457988f9765e4dfe1c0d2453c5ca4eabc897d4309672c8e014", size = 8594298, upload-time = "2025-05-08T19:10:51.738Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +] + +[[package]] +name = "oauthlib" +version = "3.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/fa/fbf4001037904031639e6bfbfc02badfc7e12f137a8afa254df6c4c8a670/oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918", size = 177352, upload-time = "2022-10-17T20:04:27.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/80/cab10959dc1faead58dc8384a781dfbf93cb4d33d50988f7a69f1b7c9bbe/oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", size = 151688, upload-time = "2022-10-17T20:04:24.037Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pandas" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213, upload-time = "2024-09-20T13:10:04.827Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/70/c853aec59839bceed032d52010ff5f1b8d87dc3114b762e4ba2727661a3b/pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5", size = 12580827, upload-time = "2024-09-20T13:08:42.347Z" }, + { url = "https://files.pythonhosted.org/packages/99/f2/c4527768739ffa4469b2b4fff05aa3768a478aed89a2f271a79a40eee984/pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348", size = 11303897, upload-time = "2024-09-20T13:08:45.807Z" }, + { url = "https://files.pythonhosted.org/packages/ed/12/86c1747ea27989d7a4064f806ce2bae2c6d575b950be087837bdfcabacc9/pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed", size = 66480908, upload-time = "2024-09-20T18:37:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/44/50/7db2cd5e6373ae796f0ddad3675268c8d59fb6076e66f0c339d61cea886b/pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57", size = 13064210, upload-time = "2024-09-20T13:08:48.325Z" }, + { url = "https://files.pythonhosted.org/packages/61/61/a89015a6d5536cb0d6c3ba02cebed51a95538cf83472975275e28ebf7d0c/pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42", size = 16754292, upload-time = "2024-09-20T19:01:54.443Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0d/4cc7b69ce37fac07645a94e1d4b0880b15999494372c1523508511b09e40/pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f", size = 14416379, upload-time = "2024-09-20T13:08:50.882Z" }, + { url = "https://files.pythonhosted.org/packages/31/9e/6ebb433de864a6cd45716af52a4d7a8c3c9aaf3a98368e61db9e69e69a9c/pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645", size = 11598471, upload-time = "2024-09-20T13:08:53.332Z" }, + { url = "https://files.pythonhosted.org/packages/a8/44/d9502bf0ed197ba9bf1103c9867d5904ddcaf869e52329787fc54ed70cc8/pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039", size = 12602222, upload-time = "2024-09-20T13:08:56.254Z" }, + { url = "https://files.pythonhosted.org/packages/52/11/9eac327a38834f162b8250aab32a6781339c69afe7574368fffe46387edf/pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd", size = 11321274, upload-time = "2024-09-20T13:08:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/45/fb/c4beeb084718598ba19aa9f5abbc8aed8b42f90930da861fcb1acdb54c3a/pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698", size = 15579836, upload-time = "2024-09-20T19:01:57.571Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5f/4dba1d39bb9c38d574a9a22548c540177f78ea47b32f99c0ff2ec499fac5/pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc", size = 13058505, upload-time = "2024-09-20T13:09:01.501Z" }, + { url = "https://files.pythonhosted.org/packages/b9/57/708135b90391995361636634df1f1130d03ba456e95bcf576fada459115a/pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3", size = 16744420, upload-time = "2024-09-20T19:02:00.678Z" }, + { url = "https://files.pythonhosted.org/packages/86/4a/03ed6b7ee323cf30404265c284cee9c65c56a212e0a08d9ee06984ba2240/pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32", size = 14440457, upload-time = "2024-09-20T13:09:04.105Z" }, + { url = "https://files.pythonhosted.org/packages/ed/8c/87ddf1fcb55d11f9f847e3c69bb1c6f8e46e2f40ab1a2d2abadb2401b007/pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5", size = 11617166, upload-time = "2024-09-20T13:09:06.917Z" }, + { url = "https://files.pythonhosted.org/packages/17/a3/fb2734118db0af37ea7433f57f722c0a56687e14b14690edff0cdb4b7e58/pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9", size = 12529893, upload-time = "2024-09-20T13:09:09.655Z" }, + { url = "https://files.pythonhosted.org/packages/e1/0c/ad295fd74bfac85358fd579e271cded3ac969de81f62dd0142c426b9da91/pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4", size = 11363475, upload-time = "2024-09-20T13:09:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/c6/2a/4bba3f03f7d07207481fed47f5b35f556c7441acddc368ec43d6643c5777/pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3", size = 15188645, upload-time = "2024-09-20T19:02:03.88Z" }, + { url = "https://files.pythonhosted.org/packages/38/f8/d8fddee9ed0d0c0f4a2132c1dfcf0e3e53265055da8df952a53e7eaf178c/pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319", size = 12739445, upload-time = "2024-09-20T13:09:17.621Z" }, + { url = "https://files.pythonhosted.org/packages/20/e8/45a05d9c39d2cea61ab175dbe6a2de1d05b679e8de2011da4ee190d7e748/pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8", size = 16359235, upload-time = "2024-09-20T19:02:07.094Z" }, + { url = "https://files.pythonhosted.org/packages/1d/99/617d07a6a5e429ff90c90da64d428516605a1ec7d7bea494235e1c3882de/pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a", size = 14056756, upload-time = "2024-09-20T13:09:20.474Z" }, + { url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248, upload-time = "2024-09-20T13:09:23.137Z" }, + { url = "https://files.pythonhosted.org/packages/64/22/3b8f4e0ed70644e85cfdcd57454686b9057c6c38d2f74fe4b8bc2527214a/pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015", size = 12477643, upload-time = "2024-09-20T13:09:25.522Z" }, + { url = "https://files.pythonhosted.org/packages/e4/93/b3f5d1838500e22c8d793625da672f3eec046b1a99257666c94446969282/pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28", size = 11281573, upload-time = "2024-09-20T13:09:28.012Z" }, + { url = "https://files.pythonhosted.org/packages/f5/94/6c79b07f0e5aab1dcfa35a75f4817f5c4f677931d4234afcd75f0e6a66ca/pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0", size = 15196085, upload-time = "2024-09-20T19:02:10.451Z" }, + { url = "https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24", size = 12711809, upload-time = "2024-09-20T13:09:30.814Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7c/c6dbdb0cb2a4344cacfb8de1c5808ca885b2e4dcfde8008266608f9372af/pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659", size = 16356316, upload-time = "2024-09-20T19:02:13.825Z" }, + { url = "https://files.pythonhosted.org/packages/57/b7/8b757e7d92023b832869fa8881a992696a0bfe2e26f72c9ae9f255988d42/pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb", size = 14022055, upload-time = "2024-09-20T13:09:33.462Z" }, + { url = "https://files.pythonhosted.org/packages/3b/bc/4b18e2b8c002572c5a441a64826252ce5da2aa738855747247a971988043/pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d", size = 11481175, upload-time = "2024-09-20T13:09:35.871Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/a5d88146815e972d40d19247b2c162e88213ef51c7c25993942c39dbf41d/pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468", size = 12615650, upload-time = "2024-09-20T13:09:38.685Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8c/f0fd18f6140ddafc0c24122c8a964e48294acc579d47def376fef12bcb4a/pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18", size = 11290177, upload-time = "2024-09-20T13:09:41.141Z" }, + { url = "https://files.pythonhosted.org/packages/ed/f9/e995754eab9c0f14c6777401f7eece0943840b7a9fc932221c19d1abee9f/pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2", size = 14651526, upload-time = "2024-09-20T19:02:16.905Z" }, + { url = "https://files.pythonhosted.org/packages/25/b0/98d6ae2e1abac4f35230aa756005e8654649d305df9a28b16b9ae4353bff/pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4", size = 11871013, upload-time = "2024-09-20T13:09:44.39Z" }, + { url = "https://files.pythonhosted.org/packages/cc/57/0f72a10f9db6a4628744c8e8f0df4e6e21de01212c7c981d31e50ffc8328/pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d", size = 15711620, upload-time = "2024-09-20T19:02:20.639Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436, upload-time = "2024-09-20T13:09:48.112Z" }, +] + +[[package]] +name = "pillow" +version = "11.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/cb/bb5c01fcd2a69335b86c22142b2bccfc3464087efb7fd382eee5ffc7fdf7/pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6", size = 47026707, upload-time = "2025-04-12T17:50:03.289Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/8b/b158ad57ed44d3cc54db8d68ad7c0a58b8fc0e4c7a3f995f9d62d5b464a1/pillow-11.2.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:d57a75d53922fc20c165016a20d9c44f73305e67c351bbc60d1adaf662e74047", size = 3198442, upload-time = "2025-04-12T17:47:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f8/bb5d956142f86c2d6cc36704943fa761f2d2e4c48b7436fd0a85c20f1713/pillow-11.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:127bf6ac4a5b58b3d32fc8289656f77f80567d65660bc46f72c0d77e6600cc95", size = 3030553, upload-time = "2025-04-12T17:47:13.153Z" }, + { url = "https://files.pythonhosted.org/packages/22/7f/0e413bb3e2aa797b9ca2c5c38cb2e2e45d88654e5b12da91ad446964cfae/pillow-11.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4ba4be812c7a40280629e55ae0b14a0aafa150dd6451297562e1764808bbe61", size = 4405503, upload-time = "2025-04-12T17:47:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b4/cc647f4d13f3eb837d3065824aa58b9bcf10821f029dc79955ee43f793bd/pillow-11.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8bd62331e5032bc396a93609982a9ab6b411c05078a52f5fe3cc59234a3abd1", size = 4490648, upload-time = "2025-04-12T17:47:17.37Z" }, + { url = "https://files.pythonhosted.org/packages/c2/6f/240b772a3b35cdd7384166461567aa6713799b4e78d180c555bd284844ea/pillow-11.2.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:562d11134c97a62fe3af29581f083033179f7ff435f78392565a1ad2d1c2c45c", size = 4508937, upload-time = "2025-04-12T17:47:19.066Z" }, + { url = "https://files.pythonhosted.org/packages/f3/5e/7ca9c815ade5fdca18853db86d812f2f188212792780208bdb37a0a6aef4/pillow-11.2.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:c97209e85b5be259994eb5b69ff50c5d20cca0f458ef9abd835e262d9d88b39d", size = 4599802, upload-time = "2025-04-12T17:47:21.404Z" }, + { url = "https://files.pythonhosted.org/packages/02/81/c3d9d38ce0c4878a77245d4cf2c46d45a4ad0f93000227910a46caff52f3/pillow-11.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0c3e6d0f59171dfa2e25d7116217543310908dfa2770aa64b8f87605f8cacc97", size = 4576717, upload-time = "2025-04-12T17:47:23.571Z" }, + { url = "https://files.pythonhosted.org/packages/42/49/52b719b89ac7da3185b8d29c94d0e6aec8140059e3d8adcaa46da3751180/pillow-11.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc1c3bc53befb6096b84165956e886b1729634a799e9d6329a0c512ab651e579", size = 4654874, upload-time = "2025-04-12T17:47:25.783Z" }, + { url = "https://files.pythonhosted.org/packages/5b/0b/ede75063ba6023798267023dc0d0401f13695d228194d2242d5a7ba2f964/pillow-11.2.1-cp310-cp310-win32.whl", hash = "sha256:312c77b7f07ab2139924d2639860e084ec2a13e72af54d4f08ac843a5fc9c79d", size = 2331717, upload-time = "2025-04-12T17:47:28.922Z" }, + { url = "https://files.pythonhosted.org/packages/ed/3c/9831da3edea527c2ed9a09f31a2c04e77cd705847f13b69ca60269eec370/pillow-11.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:9bc7ae48b8057a611e5fe9f853baa88093b9a76303937449397899385da06fad", size = 2676204, upload-time = "2025-04-12T17:47:31.283Z" }, + { url = "https://files.pythonhosted.org/packages/01/97/1f66ff8a1503d8cbfc5bae4dc99d54c6ec1e22ad2b946241365320caabc2/pillow-11.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:2728567e249cdd939f6cc3d1f049595c66e4187f3c34078cbc0a7d21c47482d2", size = 2414767, upload-time = "2025-04-12T17:47:34.655Z" }, + { url = "https://files.pythonhosted.org/packages/68/08/3fbf4b98924c73037a8e8b4c2c774784805e0fb4ebca6c5bb60795c40125/pillow-11.2.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35ca289f712ccfc699508c4658a1d14652e8033e9b69839edf83cbdd0ba39e70", size = 3198450, upload-time = "2025-04-12T17:47:37.135Z" }, + { url = "https://files.pythonhosted.org/packages/84/92/6505b1af3d2849d5e714fc75ba9e69b7255c05ee42383a35a4d58f576b16/pillow-11.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0409af9f829f87a2dfb7e259f78f317a5351f2045158be321fd135973fff7bf", size = 3030550, upload-time = "2025-04-12T17:47:39.345Z" }, + { url = "https://files.pythonhosted.org/packages/3c/8c/ac2f99d2a70ff966bc7eb13dacacfaab57c0549b2ffb351b6537c7840b12/pillow-11.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4e5c5edee874dce4f653dbe59db7c73a600119fbea8d31f53423586ee2aafd7", size = 4415018, upload-time = "2025-04-12T17:47:41.128Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e3/0a58b5d838687f40891fff9cbaf8669f90c96b64dc8f91f87894413856c6/pillow-11.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b93a07e76d13bff9444f1a029e0af2964e654bfc2e2c2d46bfd080df5ad5f3d8", size = 4498006, upload-time = "2025-04-12T17:47:42.912Z" }, + { url = "https://files.pythonhosted.org/packages/21/f5/6ba14718135f08fbfa33308efe027dd02b781d3f1d5c471444a395933aac/pillow-11.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:e6def7eed9e7fa90fde255afaf08060dc4b343bbe524a8f69bdd2a2f0018f600", size = 4517773, upload-time = "2025-04-12T17:47:44.611Z" }, + { url = "https://files.pythonhosted.org/packages/20/f2/805ad600fc59ebe4f1ba6129cd3a75fb0da126975c8579b8f57abeb61e80/pillow-11.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8f4f3724c068be008c08257207210c138d5f3731af6c155a81c2b09a9eb3a788", size = 4607069, upload-time = "2025-04-12T17:47:46.46Z" }, + { url = "https://files.pythonhosted.org/packages/71/6b/4ef8a288b4bb2e0180cba13ca0a519fa27aa982875882392b65131401099/pillow-11.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0a6709b47019dff32e678bc12c63008311b82b9327613f534e496dacaefb71e", size = 4583460, upload-time = "2025-04-12T17:47:49.255Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/f29c705a09cbc9e2a456590816e5c234382ae5d32584f451c3eb41a62062/pillow-11.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f6b0c664ccb879109ee3ca702a9272d877f4fcd21e5eb63c26422fd6e415365e", size = 4661304, upload-time = "2025-04-12T17:47:51.067Z" }, + { url = "https://files.pythonhosted.org/packages/6e/1a/c8217b6f2f73794a5e219fbad087701f412337ae6dbb956db37d69a9bc43/pillow-11.2.1-cp311-cp311-win32.whl", hash = "sha256:cc5d875d56e49f112b6def6813c4e3d3036d269c008bf8aef72cd08d20ca6df6", size = 2331809, upload-time = "2025-04-12T17:47:54.425Z" }, + { url = "https://files.pythonhosted.org/packages/e2/72/25a8f40170dc262e86e90f37cb72cb3de5e307f75bf4b02535a61afcd519/pillow-11.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:0f5c7eda47bf8e3c8a283762cab94e496ba977a420868cb819159980b6709193", size = 2676338, upload-time = "2025-04-12T17:47:56.535Z" }, + { url = "https://files.pythonhosted.org/packages/06/9e/76825e39efee61efea258b479391ca77d64dbd9e5804e4ad0fa453b4ba55/pillow-11.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:4d375eb838755f2528ac8cbc926c3e31cc49ca4ad0cf79cff48b20e30634a4a7", size = 2414918, upload-time = "2025-04-12T17:47:58.217Z" }, + { url = "https://files.pythonhosted.org/packages/c7/40/052610b15a1b8961f52537cc8326ca6a881408bc2bdad0d852edeb6ed33b/pillow-11.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f", size = 3190185, upload-time = "2025-04-12T17:48:00.417Z" }, + { url = "https://files.pythonhosted.org/packages/e5/7e/b86dbd35a5f938632093dc40d1682874c33dcfe832558fc80ca56bfcb774/pillow-11.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b", size = 3030306, upload-time = "2025-04-12T17:48:02.391Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5c/467a161f9ed53e5eab51a42923c33051bf8d1a2af4626ac04f5166e58e0c/pillow-11.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d", size = 4416121, upload-time = "2025-04-12T17:48:04.554Z" }, + { url = "https://files.pythonhosted.org/packages/62/73/972b7742e38ae0e2ac76ab137ca6005dcf877480da0d9d61d93b613065b4/pillow-11.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4", size = 4501707, upload-time = "2025-04-12T17:48:06.831Z" }, + { url = "https://files.pythonhosted.org/packages/e4/3a/427e4cb0b9e177efbc1a84798ed20498c4f233abde003c06d2650a6d60cb/pillow-11.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d", size = 4522921, upload-time = "2025-04-12T17:48:09.229Z" }, + { url = "https://files.pythonhosted.org/packages/fe/7c/d8b1330458e4d2f3f45d9508796d7caf0c0d3764c00c823d10f6f1a3b76d/pillow-11.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4", size = 4612523, upload-time = "2025-04-12T17:48:11.631Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2f/65738384e0b1acf451de5a573d8153fe84103772d139e1e0bdf1596be2ea/pillow-11.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443", size = 4587836, upload-time = "2025-04-12T17:48:13.592Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c5/e795c9f2ddf3debb2dedd0df889f2fe4b053308bb59a3cc02a0cd144d641/pillow-11.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c", size = 4669390, upload-time = "2025-04-12T17:48:15.938Z" }, + { url = "https://files.pythonhosted.org/packages/96/ae/ca0099a3995976a9fce2f423166f7bff9b12244afdc7520f6ed38911539a/pillow-11.2.1-cp312-cp312-win32.whl", hash = "sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3", size = 2332309, upload-time = "2025-04-12T17:48:17.885Z" }, + { url = "https://files.pythonhosted.org/packages/7c/18/24bff2ad716257fc03da964c5e8f05d9790a779a8895d6566e493ccf0189/pillow-11.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941", size = 2676768, upload-time = "2025-04-12T17:48:19.655Z" }, + { url = "https://files.pythonhosted.org/packages/da/bb/e8d656c9543276517ee40184aaa39dcb41e683bca121022f9323ae11b39d/pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb", size = 2415087, upload-time = "2025-04-12T17:48:21.991Z" }, + { url = "https://files.pythonhosted.org/packages/36/9c/447528ee3776e7ab8897fe33697a7ff3f0475bb490c5ac1456a03dc57956/pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28", size = 3190098, upload-time = "2025-04-12T17:48:23.915Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/29d5cd052f7566a63e5b506fac9c60526e9ecc553825551333e1e18a4858/pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830", size = 3030166, upload-time = "2025-04-12T17:48:25.738Z" }, + { url = "https://files.pythonhosted.org/packages/71/5d/446ee132ad35e7600652133f9c2840b4799bbd8e4adba881284860da0a36/pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0", size = 4408674, upload-time = "2025-04-12T17:48:27.908Z" }, + { url = "https://files.pythonhosted.org/packages/69/5f/cbe509c0ddf91cc3a03bbacf40e5c2339c4912d16458fcb797bb47bcb269/pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1", size = 4496005, upload-time = "2025-04-12T17:48:29.888Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b3/dd4338d8fb8a5f312021f2977fb8198a1184893f9b00b02b75d565c33b51/pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f", size = 4518707, upload-time = "2025-04-12T17:48:31.874Z" }, + { url = "https://files.pythonhosted.org/packages/13/eb/2552ecebc0b887f539111c2cd241f538b8ff5891b8903dfe672e997529be/pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155", size = 4610008, upload-time = "2025-04-12T17:48:34.422Z" }, + { url = "https://files.pythonhosted.org/packages/72/d1/924ce51bea494cb6e7959522d69d7b1c7e74f6821d84c63c3dc430cbbf3b/pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14", size = 4585420, upload-time = "2025-04-12T17:48:37.641Z" }, + { url = "https://files.pythonhosted.org/packages/43/ab/8f81312d255d713b99ca37479a4cb4b0f48195e530cdc1611990eb8fd04b/pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b", size = 4667655, upload-time = "2025-04-12T17:48:39.652Z" }, + { url = "https://files.pythonhosted.org/packages/94/86/8f2e9d2dc3d308dfd137a07fe1cc478df0a23d42a6c4093b087e738e4827/pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2", size = 2332329, upload-time = "2025-04-12T17:48:41.765Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ec/1179083b8d6067a613e4d595359b5fdea65d0a3b7ad623fee906e1b3c4d2/pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691", size = 2676388, upload-time = "2025-04-12T17:48:43.625Z" }, + { url = "https://files.pythonhosted.org/packages/23/f1/2fc1e1e294de897df39fa8622d829b8828ddad938b0eaea256d65b84dd72/pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c", size = 2414950, upload-time = "2025-04-12T17:48:45.475Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3e/c328c48b3f0ead7bab765a84b4977acb29f101d10e4ef57a5e3400447c03/pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22", size = 3192759, upload-time = "2025-04-12T17:48:47.866Z" }, + { url = "https://files.pythonhosted.org/packages/18/0e/1c68532d833fc8b9f404d3a642991441d9058eccd5606eab31617f29b6d4/pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7", size = 3033284, upload-time = "2025-04-12T17:48:50.189Z" }, + { url = "https://files.pythonhosted.org/packages/b7/cb/6faf3fb1e7705fd2db74e070f3bf6f88693601b0ed8e81049a8266de4754/pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16", size = 4445826, upload-time = "2025-04-12T17:48:52.346Z" }, + { url = "https://files.pythonhosted.org/packages/07/94/8be03d50b70ca47fb434a358919d6a8d6580f282bbb7af7e4aa40103461d/pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b", size = 4527329, upload-time = "2025-04-12T17:48:54.403Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a4/bfe78777076dc405e3bd2080bc32da5ab3945b5a25dc5d8acaa9de64a162/pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406", size = 4549049, upload-time = "2025-04-12T17:48:56.383Z" }, + { url = "https://files.pythonhosted.org/packages/65/4d/eaf9068dc687c24979e977ce5677e253624bd8b616b286f543f0c1b91662/pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91", size = 4635408, upload-time = "2025-04-12T17:48:58.782Z" }, + { url = "https://files.pythonhosted.org/packages/1d/26/0fd443365d9c63bc79feb219f97d935cd4b93af28353cba78d8e77b61719/pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751", size = 4614863, upload-time = "2025-04-12T17:49:00.709Z" }, + { url = "https://files.pythonhosted.org/packages/49/65/dca4d2506be482c2c6641cacdba5c602bc76d8ceb618fd37de855653a419/pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9", size = 4692938, upload-time = "2025-04-12T17:49:02.946Z" }, + { url = "https://files.pythonhosted.org/packages/b3/92/1ca0c3f09233bd7decf8f7105a1c4e3162fb9142128c74adad0fb361b7eb/pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd", size = 2335774, upload-time = "2025-04-12T17:49:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ac/77525347cb43b83ae905ffe257bbe2cc6fd23acb9796639a1f56aa59d191/pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e", size = 2681895, upload-time = "2025-04-12T17:49:06.635Z" }, + { url = "https://files.pythonhosted.org/packages/67/32/32dc030cfa91ca0fc52baebbba2e009bb001122a1daa8b6a79ad830b38d3/pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681", size = 2417234, upload-time = "2025-04-12T17:49:08.399Z" }, + { url = "https://files.pythonhosted.org/packages/33/49/c8c21e4255b4f4a2c0c68ac18125d7f5460b109acc6dfdef1a24f9b960ef/pillow-11.2.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9b7b0d4fd2635f54ad82785d56bc0d94f147096493a79985d0ab57aedd563156", size = 3181727, upload-time = "2025-04-12T17:49:31.898Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f1/f7255c0838f8c1ef6d55b625cfb286835c17e8136ce4351c5577d02c443b/pillow-11.2.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:aa442755e31c64037aa7c1cb186e0b369f8416c567381852c63444dd666fb772", size = 2999833, upload-time = "2025-04-12T17:49:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/e2/57/9968114457bd131063da98d87790d080366218f64fa2943b65ac6739abb3/pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0d3348c95b766f54b76116d53d4cb171b52992a1027e7ca50c81b43b9d9e363", size = 3437472, upload-time = "2025-04-12T17:49:36.294Z" }, + { url = "https://files.pythonhosted.org/packages/b2/1b/e35d8a158e21372ecc48aac9c453518cfe23907bb82f950d6e1c72811eb0/pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85d27ea4c889342f7e35f6d56e7e1cb345632ad592e8c51b693d7b7556043ce0", size = 3459976, upload-time = "2025-04-12T17:49:38.988Z" }, + { url = "https://files.pythonhosted.org/packages/26/da/2c11d03b765efff0ccc473f1c4186dc2770110464f2177efaed9cf6fae01/pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bf2c33d6791c598142f00c9c4c7d47f6476731c31081331664eb26d6ab583e01", size = 3527133, upload-time = "2025-04-12T17:49:40.985Z" }, + { url = "https://files.pythonhosted.org/packages/79/1a/4e85bd7cadf78412c2a3069249a09c32ef3323650fd3005c97cca7aa21df/pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e616e7154c37669fc1dfc14584f11e284e05d1c650e1c0f972f281c4ccc53193", size = 3571555, upload-time = "2025-04-12T17:49:42.964Z" }, + { url = "https://files.pythonhosted.org/packages/69/03/239939915216de1e95e0ce2334bf17a7870ae185eb390fab6d706aadbfc0/pillow-11.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:39ad2e0f424394e3aebc40168845fee52df1394a4673a6ee512d840d14ab3013", size = 2674713, upload-time = "2025-04-12T17:49:44.944Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ad/2613c04633c7257d9481ab21d6b5364b59fc5d75faafd7cb8693523945a3/pillow-11.2.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:80f1df8dbe9572b4b7abdfa17eb5d78dd620b1d55d9e25f834efdbee872d3aed", size = 3181734, upload-time = "2025-04-12T17:49:46.789Z" }, + { url = "https://files.pythonhosted.org/packages/a4/fd/dcdda4471ed667de57bb5405bb42d751e6cfdd4011a12c248b455c778e03/pillow-11.2.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ea926cfbc3957090becbcbbb65ad177161a2ff2ad578b5a6ec9bb1e1cd78753c", size = 2999841, upload-time = "2025-04-12T17:49:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/ac/89/8a2536e95e77432833f0db6fd72a8d310c8e4272a04461fb833eb021bf94/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:738db0e0941ca0376804d4de6a782c005245264edaa253ffce24e5a15cbdc7bd", size = 3437470, upload-time = "2025-04-12T17:49:50.831Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8f/abd47b73c60712f88e9eda32baced7bfc3e9bd6a7619bb64b93acff28c3e/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db98ab6565c69082ec9b0d4e40dd9f6181dab0dd236d26f7a50b8b9bfbd5076", size = 3460013, upload-time = "2025-04-12T17:49:53.278Z" }, + { url = "https://files.pythonhosted.org/packages/f6/20/5c0a0aa83b213b7a07ec01e71a3d6ea2cf4ad1d2c686cc0168173b6089e7/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:036e53f4170e270ddb8797d4c590e6dd14d28e15c7da375c18978045f7e6c37b", size = 3527165, upload-time = "2025-04-12T17:49:55.164Z" }, + { url = "https://files.pythonhosted.org/packages/58/0e/2abab98a72202d91146abc839e10c14f7cf36166f12838ea0c4db3ca6ecb/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:14f73f7c291279bd65fda51ee87affd7c1e097709f7fdd0188957a16c264601f", size = 3571586, upload-time = "2025-04-12T17:49:57.171Z" }, + { url = "https://files.pythonhosted.org/packages/21/2c/5e05f58658cf49b6667762cca03d6e7d85cededde2caf2ab37b81f80e574/pillow-11.2.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:208653868d5c9ecc2b327f9b9ef34e0e42a4cdd172c2988fd81d62d2bc9bc044", size = 2674751, upload-time = "2025-04-12T17:49:59.628Z" }, +] + +[[package]] +name = "psutil" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload-time = "2025-02-13T21:54:07.946Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload-time = "2025-02-13T21:54:12.36Z" }, + { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload-time = "2025-02-13T21:54:16.07Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload-time = "2025-02-13T21:54:18.662Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload-time = "2025-02-13T21:54:21.811Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload-time = "2025-02-13T21:54:24.68Z" }, + { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload-time = "2025-02-13T21:54:34.31Z" }, + { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608, upload-time = "2025-03-25T05:01:28.114Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120, upload-time = "2025-03-25T05:01:24.908Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, +] + +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "oauthlib" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, +] + +[[package]] +name = "rsa" +version = "4.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, +] + +[[package]] +name = "scipy" +version = "1.15.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770, upload-time = "2025-05-08T16:04:20.849Z" }, + { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511, upload-time = "2025-05-08T16:04:27.103Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151, upload-time = "2025-05-08T16:04:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732, upload-time = "2025-05-08T16:04:36.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617, upload-time = "2025-05-08T16:04:43.546Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964, upload-time = "2025-05-08T16:04:49.431Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749, upload-time = "2025-05-08T16:04:55.215Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383, upload-time = "2025-05-08T16:05:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201, upload-time = "2025-05-08T16:05:08.166Z" }, + { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" }, + { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" }, + { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" }, + { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" }, + { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" }, + { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" }, + { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" }, + { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" }, + { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" }, + { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256, upload-time = "2025-05-08T16:06:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540, upload-time = "2025-05-08T16:07:04.209Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115, upload-time = "2025-05-08T16:07:08.998Z" }, + { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884, upload-time = "2025-05-08T16:07:14.091Z" }, + { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018, upload-time = "2025-05-08T16:07:19.427Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716, upload-time = "2025-05-08T16:07:25.712Z" }, + { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342, upload-time = "2025-05-08T16:07:31.468Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869, upload-time = "2025-05-08T16:07:38.002Z" }, + { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851, upload-time = "2025-05-08T16:08:33.671Z" }, + { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011, upload-time = "2025-05-08T16:07:44.039Z" }, + { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407, upload-time = "2025-05-08T16:07:49.891Z" }, + { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030, upload-time = "2025-05-08T16:07:54.121Z" }, + { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709, upload-time = "2025-05-08T16:07:58.506Z" }, + { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045, upload-time = "2025-05-08T16:08:03.929Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062, upload-time = "2025-05-08T16:08:09.558Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132, upload-time = "2025-05-08T16:08:15.34Z" }, + { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503, upload-time = "2025-05-08T16:08:21.513Z" }, + { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, +] + +[[package]] +name = "urllib3" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" }, +] From 837c5058287e8d96d7682ee7d4ace99414b41ae7 Mon Sep 17 00:00:00 2001 From: Ajasra Date: Tue, 20 May 2025 16:59:17 +0800 Subject: [PATCH 2/6] Refactor --- cycles/__init__.py | 0 charts.py => cycles/charts.py | 0 main_debug.py => cycles/main_debug.py | 0 taxes.py => cycles/taxes.py | 0 .../trend_detector_simple.py | 1696 ++++++++--------- cycles/utils/__init__.py | 0 .../utils}/apply_taxes_to_file.py | 0 cycles/utils/gsheets.py | 128 ++ cycles/utils/storage.py | 155 ++ cycles/utils/system.py | 19 + main.py | 264 +-- 11 files changed, 1174 insertions(+), 1088 deletions(-) create mode 100644 cycles/__init__.py rename charts.py => cycles/charts.py (100%) rename main_debug.py => cycles/main_debug.py (100%) rename taxes.py => cycles/taxes.py (100%) rename trend_detector_simple.py => cycles/trend_detector_simple.py (97%) create mode 100644 cycles/utils/__init__.py rename {utils => cycles/utils}/apply_taxes_to_file.py (100%) create mode 100644 cycles/utils/gsheets.py create mode 100644 cycles/utils/storage.py create mode 100644 cycles/utils/system.py diff --git a/cycles/__init__.py b/cycles/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/charts.py b/cycles/charts.py similarity index 100% rename from charts.py rename to cycles/charts.py diff --git a/main_debug.py b/cycles/main_debug.py similarity index 100% rename from main_debug.py rename to cycles/main_debug.py diff --git a/taxes.py b/cycles/taxes.py similarity index 100% rename from taxes.py rename to cycles/taxes.py diff --git a/trend_detector_simple.py b/cycles/trend_detector_simple.py similarity index 97% rename from trend_detector_simple.py rename to cycles/trend_detector_simple.py index 95a1bb5..d885959 100644 --- a/trend_detector_simple.py +++ b/cycles/trend_detector_simple.py @@ -1,849 +1,849 @@ -import pandas as pd -import numpy as np -import logging -from scipy.signal import find_peaks -from matplotlib.patches import Rectangle -from scipy import stats -import concurrent.futures -from functools import partial -from functools import lru_cache -import matplotlib.pyplot as plt - -# Color configuration -# Plot colors -DARK_BG_COLOR = '#181C27' -LEGEND_BG_COLOR = '#333333' -TITLE_COLOR = 'white' -AXIS_LABEL_COLOR = 'white' - -# Candlestick colors -CANDLE_UP_COLOR = '#089981' # Green -CANDLE_DOWN_COLOR = '#F23645' # Red - -# Marker colors -MIN_COLOR = 'red' -MAX_COLOR = 'green' - -# Line style colors -MIN_LINE_STYLE = 'g--' # Green dashed -MAX_LINE_STYLE = 'r--' # Red dashed -SMA7_LINE_STYLE = 'y-' # Yellow solid -SMA15_LINE_STYLE = 'm-' # Magenta solid - -# SuperTrend colors -ST_COLOR_UP = 'g-' -ST_COLOR_DOWN = 'r-' - -# Cache the calculation results by function parameters -@lru_cache(maxsize=32) -def cached_supertrend_calculation(period, multiplier, data_tuple): - # Convert tuple back to numpy arrays - high = np.array(data_tuple[0]) - low = np.array(data_tuple[1]) - close = np.array(data_tuple[2]) - - # Calculate TR and ATR using vectorized operations - tr = np.zeros_like(close) - tr[0] = high[0] - low[0] - hc_range = np.abs(high[1:] - close[:-1]) - lc_range = np.abs(low[1:] - close[:-1]) - hl_range = high[1:] - low[1:] - tr[1:] = np.maximum.reduce([hl_range, hc_range, lc_range]) - - # Use numpy's exponential moving average - atr = np.zeros_like(tr) - atr[0] = tr[0] - multiplier_ema = 2.0 / (period + 1) - for i in range(1, len(tr)): - atr[i] = (tr[i] * multiplier_ema) + (atr[i-1] * (1 - multiplier_ema)) - - # Calculate bands - upper_band = np.zeros_like(close) - lower_band = np.zeros_like(close) - for i in range(len(close)): - hl_avg = (high[i] + low[i]) / 2 - upper_band[i] = hl_avg + (multiplier * atr[i]) - lower_band[i] = hl_avg - (multiplier * atr[i]) - - final_upper = np.zeros_like(close) - final_lower = np.zeros_like(close) - supertrend = np.zeros_like(close) - trend = np.zeros_like(close) - final_upper[0] = upper_band[0] - final_lower[0] = lower_band[0] - if close[0] <= upper_band[0]: - supertrend[0] = upper_band[0] - trend[0] = -1 - else: - supertrend[0] = lower_band[0] - trend[0] = 1 - for i in range(1, len(close)): - if (upper_band[i] < final_upper[i-1]) or (close[i-1] > final_upper[i-1]): - final_upper[i] = upper_band[i] - else: - final_upper[i] = final_upper[i-1] - if (lower_band[i] > final_lower[i-1]) or (close[i-1] < final_lower[i-1]): - final_lower[i] = lower_band[i] - else: - final_lower[i] = final_lower[i-1] - if supertrend[i-1] == final_upper[i-1] and close[i] <= final_upper[i]: - supertrend[i] = final_upper[i] - trend[i] = -1 - elif supertrend[i-1] == final_upper[i-1] and close[i] > final_upper[i]: - supertrend[i] = final_lower[i] - trend[i] = 1 - elif supertrend[i-1] == final_lower[i-1] and close[i] >= final_lower[i]: - supertrend[i] = final_lower[i] - trend[i] = 1 - elif supertrend[i-1] == final_lower[i-1] and close[i] < final_lower[i]: - supertrend[i] = final_upper[i] - trend[i] = -1 - return { - 'supertrend': supertrend, - 'trend': trend, - 'upper_band': final_upper, - 'lower_band': final_lower - } - -def calculate_supertrend_external(data, period, multiplier): - # Convert DataFrame columns to hashable tuples - high_tuple = tuple(data['high']) - low_tuple = tuple(data['low']) - close_tuple = tuple(data['close']) - - # Call the cached function - return cached_supertrend_calculation(period, multiplier, (high_tuple, low_tuple, close_tuple)) - -class TrendDetectorSimple: - def __init__(self, data, verbose=False, display=False): - """ - Initialize the TrendDetectorSimple class. - - Parameters: - - data: pandas DataFrame containing price data - - verbose: boolean, whether to display detailed logging information - - display: boolean, whether to enable display/plotting features - """ - - self.data = data - self.verbose = verbose - self.display = display - - # Only define display-related variables if display is True - if self.display: - # Plot style configuration - self.plot_style = 'dark_background' - self.bg_color = DARK_BG_COLOR - self.plot_size = (12, 8) - - # Candlestick configuration - self.candle_width = 0.6 - self.candle_up_color = CANDLE_UP_COLOR - self.candle_down_color = CANDLE_DOWN_COLOR - self.candle_alpha = 0.8 - self.wick_width = 1 - - # Marker configuration - self.min_marker = '^' - self.min_color = MIN_COLOR - self.min_size = 100 - self.max_marker = 'v' - self.max_color = MAX_COLOR - self.max_size = 100 - self.marker_zorder = 100 - - # Line configuration - self.line_width = 1 - self.min_line_style = MIN_LINE_STYLE - self.max_line_style = MAX_LINE_STYLE - self.sma7_line_style = SMA7_LINE_STYLE - self.sma15_line_style = SMA15_LINE_STYLE - - # Text configuration - self.title_size = 14 - self.title_color = TITLE_COLOR - self.axis_label_size = 12 - self.axis_label_color = AXIS_LABEL_COLOR - - # Legend configuration - self.legend_loc = 'best' - self.legend_bg_color = LEGEND_BG_COLOR - - # Configure logging - logging.basicConfig(level=logging.INFO if verbose else logging.WARNING, - format='%(asctime)s - %(levelname)s - %(message)s') - self.logger = logging.getLogger('TrendDetectorSimple') - - # Convert data to pandas DataFrame if it's not already - if not isinstance(self.data, pd.DataFrame): - if isinstance(self.data, list): - self.data = pd.DataFrame({'close': self.data}) - else: - raise ValueError("Data must be a pandas DataFrame or a list") - - def calculate_tr(self): - """ - Calculate True Range (TR) for the price data. - - True Range is the greatest of: - 1. Current high - current low - 2. |Current high - previous close| - 3. |Current low - previous close| - - Returns: - - Numpy array of TR values - """ - df = self.data.copy() - high = df['high'].values - low = df['low'].values - close = df['close'].values - - tr = np.zeros_like(close) - tr[0] = high[0] - low[0] # First TR is just the first day's range - - for i in range(1, len(close)): - # Current high - current low - hl_range = high[i] - low[i] - # |Current high - previous close| - hc_range = abs(high[i] - close[i-1]) - # |Current low - previous close| - lc_range = abs(low[i] - close[i-1]) - - # TR is the maximum of these three values - tr[i] = max(hl_range, hc_range, lc_range) - - return tr - - def calculate_atr(self, period=14): - """ - Calculate Average True Range (ATR) for the price data. - - ATR is the exponential moving average of the True Range over a specified period. - - Parameters: - - period: int, the period for the ATR calculation (default: 14) - - Returns: - - Numpy array of ATR values - """ - - tr = self.calculate_tr() - atr = np.zeros_like(tr) - - # First ATR value is just the first TR - atr[0] = tr[0] - - # Calculate exponential moving average (EMA) of TR - multiplier = 2.0 / (period + 1) - - for i in range(1, len(tr)): - atr[i] = (tr[i] * multiplier) + (atr[i-1] * (1 - multiplier)) - - return atr - - def detect_trends(self): - """ - Detect trends by identifying local minima and maxima in the price data - using scipy.signal.find_peaks. - - Parameters: - - prominence: float, required prominence of peaks (relative to the price range) - - width: int, required width of peaks in data points - - Returns: - - DataFrame with columns for timestamps, prices, and trend indicators - - Dictionary containing analysis results including linear regression, SMAs, and SuperTrend indicators - """ - df = self.data - # close_prices = df['close'].values - - # max_peaks, _ = find_peaks(close_prices) - # min_peaks, _ = find_peaks(-close_prices) - - # df['is_min'] = False - # df['is_max'] = False - - # for peak in max_peaks: - # df.at[peak, 'is_max'] = True - # for peak in min_peaks: - # df.at[peak, 'is_min'] = True - - # result = df[['timestamp', 'close', 'is_min', 'is_max']].copy() - - # Perform linear regression on min_peaks and max_peaks - # min_prices = df['close'].iloc[min_peaks].values - # max_prices = df['close'].iloc[max_peaks].values - - # Linear regression for min peaks if we have at least 2 points - # min_slope, min_intercept, min_r_value, _, _ = stats.linregress(min_peaks, min_prices) - # Linear regression for max peaks if we have at least 2 points - # max_slope, max_intercept, max_r_value, _, _ = stats.linregress(max_peaks, max_prices) - - # Calculate Simple Moving Averages (SMA) for 7 and 15 periods - # sma_7 = pd.Series(close_prices).rolling(window=7, min_periods=1).mean().values - # sma_15 = pd.Series(close_prices).rolling(window=15, min_periods=1).mean().values - - analysis_results = {} - # analysis_results['linear_regression'] = { - # 'min': { - # 'slope': min_slope, - # 'intercept': min_intercept, - # 'r_squared': min_r_value ** 2 - # }, - # 'max': { - # 'slope': max_slope, - # 'intercept': max_intercept, - # 'r_squared': max_r_value ** 2 - # } - # } - # analysis_results['sma'] = { - # '7': sma_7, - # '15': sma_15 - # } - - # Calculate SuperTrend indicators - supertrend_results_list = self._calculate_supertrend_indicators() - analysis_results['supertrend'] = supertrend_results_list - - return analysis_results - - def _calculate_supertrend_indicators(self): - """ - Calculate SuperTrend indicators with different parameter sets in parallel. - Returns: - - list, the SuperTrend results - """ - supertrend_params = [ - {"period": 12, "multiplier": 3.0, "color_up": ST_COLOR_UP, "color_down": ST_COLOR_DOWN}, - {"period": 10, "multiplier": 1.0, "color_up": ST_COLOR_UP, "color_down": ST_COLOR_DOWN}, - {"period": 11, "multiplier": 2.0, "color_up": ST_COLOR_UP, "color_down": ST_COLOR_DOWN} - ] - data = self.data.copy() - - # For just 3 calculations, direct calculation might be faster than process pool - results = [] - for p in supertrend_params: - result = calculate_supertrend_external(data, p["period"], p["multiplier"]) - results.append(result) - - supertrend_results_list = [] - for params, result in zip(supertrend_params, results): - supertrend_results_list.append({ - "results": result, - "params": params - }) - return supertrend_results_list - - def plot_trends(self, trend_data, analysis_results, view="both"): - """ - Plot the price data with detected trends using a candlestick chart. - Also plots SuperTrend indicators with three different parameter sets. - - Parameters: - - trend_data: DataFrame, the output from detect_trends() - - analysis_results: Dictionary containing analysis results from detect_trends() - - view: str, one of 'both', 'trend', 'supertrend'; determines which plot(s) to display - - Returns: - - None (displays the plot) - """ - if not self.display: - return # Do nothing if display is False - - plt.style.use(self.plot_style) - - if view == "both": - fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(self.plot_size[0]*2, self.plot_size[1])) - else: - fig, ax = plt.subplots(figsize=self.plot_size) - ax1 = ax2 = None - if view == "trend": - ax1 = ax - elif view == "supertrend": - ax2 = ax - - fig.patch.set_facecolor(self.bg_color) - if ax1: ax1.set_facecolor(self.bg_color) - if ax2: ax2.set_facecolor(self.bg_color) - - df = self.data.copy() - - if ax1: - self._plot_trend_analysis(ax1, df, trend_data, analysis_results) - - if ax2: - self._plot_supertrend_analysis(ax2, df, analysis_results['supertrend']) - - plt.tight_layout() - plt.show() - - def _plot_candlesticks(self, ax, df): - """ - Plot candlesticks on the given axis. - - Parameters: - - ax: matplotlib.axes.Axes, the axis to plot on - - df: pandas.DataFrame, the data to plot - """ - from matplotlib.patches import Rectangle - - for i in range(len(df)): - # Get OHLC values for this candle - open_val = df['open'].iloc[i] - close_val = df['close'].iloc[i] - high_val = df['high'].iloc[i] - low_val = df['low'].iloc[i] - - # Determine candle color - color = self.candle_up_color if close_val >= open_val else self.candle_down_color - - # Plot candle body - body_height = abs(close_val - open_val) - bottom = min(open_val, close_val) - rect = Rectangle((i - self.candle_width/2, bottom), self.candle_width, body_height, - color=color, alpha=self.candle_alpha) - ax.add_patch(rect) - - # Plot candle wicks - ax.plot([i, i], [low_val, high_val], color=color, linewidth=self.wick_width) - - def _plot_trend_analysis(self, ax, df, trend_data, analysis_results): - """ - Plot trend analysis on the given axis. - - Parameters: - - ax: matplotlib.axes.Axes, the axis to plot on - - df: pandas.DataFrame, the data to plot - - trend_data: pandas.DataFrame, the trend data - - analysis_results: dict, the analysis results - """ - # Draw candlesticks - self._plot_candlesticks(ax, df) - - # Plot minima and maxima points - self._plot_min_max_points(ax, df, trend_data) - - # Plot trend lines and moving averages - if analysis_results: - self._plot_trend_lines(ax, df, analysis_results) - - # Configure the subplot - self._configure_subplot(ax, 'Price Chart with Trend Analysis', len(df)) - - def _plot_min_max_points(self, ax, df, trend_data): - """ - Plot minimum and maximum points on the given axis. - - Parameters: - - ax: matplotlib.axes.Axes, the axis to plot on - - df: pandas.DataFrame, the data to plot - - trend_data: pandas.DataFrame, the trend data - """ - min_indices = trend_data.index[trend_data['is_min'] == True].tolist() - if min_indices: - min_y = [df['close'].iloc[i] for i in min_indices] - ax.scatter(min_indices, min_y, color=self.min_color, s=self.min_size, - marker=self.min_marker, label='Local Minima', zorder=self.marker_zorder) - - max_indices = trend_data.index[trend_data['is_max'] == True].tolist() - if max_indices: - max_y = [df['close'].iloc[i] for i in max_indices] - ax.scatter(max_indices, max_y, color=self.max_color, s=self.max_size, - marker=self.max_marker, label='Local Maxima', zorder=self.marker_zorder) - - def _plot_trend_lines(self, ax, df, analysis_results): - """ - Plot trend lines on the given axis. - - Parameters: - - ax: matplotlib.axes.Axes, the axis to plot on - - df: pandas.DataFrame, the data to plot - - analysis_results: dict, the analysis results - """ - x_vals = np.arange(len(df)) - - # Minima regression line (support) - min_slope = analysis_results['linear_regression']['min']['slope'] - min_intercept = analysis_results['linear_regression']['min']['intercept'] - min_line = min_slope * x_vals + min_intercept - ax.plot(x_vals, min_line, self.min_line_style, linewidth=self.line_width, - label='Minima Regression') - - # Maxima regression line (resistance) - max_slope = analysis_results['linear_regression']['max']['slope'] - max_intercept = analysis_results['linear_regression']['max']['intercept'] - max_line = max_slope * x_vals + max_intercept - ax.plot(x_vals, max_line, self.max_line_style, linewidth=self.line_width, - label='Maxima Regression') - - # SMA-7 line - sma_7 = analysis_results['sma']['7'] - ax.plot(x_vals, sma_7, self.sma7_line_style, linewidth=self.line_width, - label='SMA-7') - - # SMA-15 line - sma_15 = analysis_results['sma']['15'] - valid_idx_15 = ~np.isnan(sma_15) - ax.plot(x_vals[valid_idx_15], sma_15[valid_idx_15], self.sma15_line_style, - linewidth=self.line_width, label='SMA-15') - - def _configure_subplot(self, ax, title, data_length): - """ - Configure the subplot with title, labels, limits, and legend. - - Parameters: - - ax: matplotlib.axes.Axes, the axis to configure - - title: str, the title of the subplot - - data_length: int, the length of the data - """ - # Set title and labels - ax.set_title(title, fontsize=self.title_size, color=self.title_color) - ax.set_xlabel('Date', fontsize=self.axis_label_size, color=self.axis_label_color) - ax.set_ylabel('Price', fontsize=self.axis_label_size, color=self.axis_label_color) - - # Set appropriate x-axis limits - ax.set_xlim(-0.5, data_length - 0.5) - - # Add a legend - ax.legend(loc=self.legend_loc, facecolor=self.legend_bg_color) - - def _plot_supertrend_analysis(self, ax, df, supertrend_results_list=None): - """ - Plot SuperTrend analysis on the given axis. - - Parameters: - - ax: matplotlib.axes.Axes, the axis to plot on - - df: pandas.DataFrame, the data to plot - - supertrend_results_list: list, the SuperTrend results (optional) - """ - self._plot_candlesticks(ax, df) - self._plot_supertrend_lines(ax, df, supertrend_results_list, style='Both') - self._configure_subplot(ax, 'Multiple SuperTrend Indicators', len(df)) - - def _plot_supertrend_lines(self, ax, df, supertrend_results_list, style="Horizontal"): - """ - Plot SuperTrend lines on the given axis. - - Parameters: - - ax: matplotlib.axes.Axes, the axis to plot on - - df: pandas.DataFrame, the data to plot - - supertrend_results_list: list, the SuperTrend results - """ - x_vals = np.arange(len(df)) - - if style == 'Horizontal' or style == 'Both': - if len(supertrend_results_list) != 3: - raise ValueError("Expected exactly 3 SuperTrend results for meta calculation") - - trends = [st["results"]["trend"] for st in supertrend_results_list] - - band_height = 0.02 * (df["high"].max() - df["low"].min()) - y_base = df["low"].min() - band_height * 1.5 - - prev_color = None - for i in range(1, len(x_vals)): - t_vals = [t[i] for t in trends] - up_count = t_vals.count(1) - down_count = t_vals.count(-1) - - if down_count == 3: - color = "red" - elif down_count == 2 and up_count == 1: - color = "orange" - elif down_count == 1 and up_count == 2: - color = "yellow" - elif up_count == 3: - color = "green" - else: - continue # skip if unknown or inconsistent values - - ax.add_patch(Rectangle( - (x_vals[i-1], y_base), - 1, - band_height, - color=color, - linewidth=0, - alpha=0.6 - )) - # Draw a vertical line at the change of color - if prev_color and prev_color != color: - ax.axvline(x_vals[i-1], color="grey", alpha=0.3, linewidth=1) - prev_color = color - - ax.set_ylim(bottom=y_base - band_height * 0.5) - if style == 'Curves' or style == 'Both': - for st in supertrend_results_list: - params = st["params"] - results = st["results"] - supertrend = results["supertrend"] - trend = results["trend"] - - # Plot SuperTrend line with color based on trend - for i in range(1, len(x_vals)): - if trend[i] == 1: # Uptrend - ax.plot(x_vals[i-1:i+1], supertrend[i-1:i+1], params["color_up"], linewidth=self.line_width) - else: # Downtrend - ax.plot(x_vals[i-1:i+1], supertrend[i-1:i+1], params["color_down"], linewidth=self.line_width) - self._plot_metasupertrend_lines(ax, df, supertrend_results_list) - self._add_supertrend_legend(ax, supertrend_results_list) - - def _plot_metasupertrend_lines(self, ax, df, supertrend_results_list): - """ - Plot a Meta SuperTrend line where all individual SuperTrends agree on trend. - - Parameters: - - ax: matplotlib.axes.Axes, the axis to plot on - - df: pandas.DataFrame, the data to plot - - supertrend_results_list: list, each item contains SuperTrend 'results' and 'params' - """ - x_vals = np.arange(len(df)) - - if len(supertrend_results_list) != 3: - raise ValueError("Expected exactly 3 SuperTrend results for meta calculation") - - trends = [st["results"]["trend"] for st in supertrend_results_list] - supertrends = [st["results"]["supertrend"] for st in supertrend_results_list] - params = supertrend_results_list[0]["params"] # Use first config for styling - - trends_arr = np.stack(trends, axis=1) - meta_trend = np.where((trends_arr[:,0] == trends_arr[:,1]) & (trends_arr[:,1] == trends_arr[:,2]), trends_arr[:,0], 0) - - for i in range(1, len(x_vals)): - t1, t2, t3 = trends[0][i], trends[1][i], trends[2][i] - if t1 == t2 == t3: - meta_trend = t1 - # Average the 3 supertrend values - st_avg_prev = np.mean([s[i-1] for s in supertrends]) - st_avg_curr = np.mean([s[i] for s in supertrends]) - color = params["color_up"] if meta_trend == 1 else params["color_down"] - ax.plot(x_vals[i-1:i+1], [st_avg_prev, st_avg_curr], color, linewidth=self.line_width) - - def _add_supertrend_legend(self, ax, supertrend_results_list): - """ - Add SuperTrend legend entries to the given axis. - - Parameters: - - ax: matplotlib.axes.Axes, the axis to add legend entries to - - supertrend_results_list: list, the SuperTrend results - """ - for st in supertrend_results_list: - params = st["params"] - period = params["period"] - multiplier = params["multiplier"] - color_up = params["color_up"] - color_down = params["color_down"] - - ax.plot([], [], color_up, linewidth=self.line_width, - label=f'ST (P:{period}, M:{multiplier}) Up') - ax.plot([], [], color_down, linewidth=self.line_width, - label=f'ST (P:{period}, M:{multiplier}) Down') - - def backtest_meta_supertrend(self, min1_df, initial_usd=10000, stop_loss_pct=0.05, transaction_cost=0.001, debug=False): - """ - Backtest a simple strategy using the meta supertrend (all three supertrends agree). - Buys when meta supertrend is positive, sells when negative, applies a percentage stop loss. - - Parameters: - - min1_df: pandas DataFrame, 1-minute timeframe data for more accurate stop loss checking (optional) - - initial_usd: float, starting USD amount - - stop_loss_pct: float, stop loss as a fraction (e.g. 0.05 for 5%) - - transaction_cost: float, transaction cost as a fraction (e.g. 0.001 for 0.1%) - - debug: bool, whether to print debug info - """ - df = self.data.copy().reset_index(drop=True) - df['timestamp'] = pd.to_datetime(df['timestamp']) - - # Get meta supertrend (all three agree) - supertrend_results_list = self._calculate_supertrend_indicators() - trends = [st['results']['trend'] for st in supertrend_results_list] - trends_arr = np.stack(trends, axis=1) - meta_trend = np.where((trends_arr[:,0] == trends_arr[:,1]) & (trends_arr[:,1] == trends_arr[:,2]), - trends_arr[:,0], 0) - - position = 0 # 0 = no position, 1 = long - entry_price = 0 - usd = initial_usd - coin = 0 - trade_log = [] - max_balance = initial_usd - drawdowns = [] - trades = [] - entry_time = None - current_trade_min1_start_idx = None - - min1_df['timestamp'] = pd.to_datetime(min1_df.index) - - for i in range(1, len(df)): - if i % 100 == 0 and debug: - self.logger.debug(f"Progress: {i}/{len(df)} rows processed.") - - price_open = df['open'].iloc[i] - price_high = df['high'].iloc[i] - price_low = df['low'].iloc[i] - price_close = df['close'].iloc[i] - date = df['timestamp'].iloc[i] - prev_mt = meta_trend[i-1] - curr_mt = meta_trend[i] - - # Check stop loss if in position - if position == 1: - stop_price = entry_price * (1 - stop_loss_pct) - - if current_trade_min1_start_idx is None: - # First check after entry, find the entry point in 1-min data - current_trade_min1_start_idx = min1_df.index[min1_df.index >= entry_time][0] - - # Get the end index for current check - current_min1_end_idx = min1_df.index[min1_df.index <= date][-1] - - # Check all 1-minute candles in between for stop loss - min1_slice = min1_df.loc[current_trade_min1_start_idx:current_min1_end_idx] - if (min1_slice['low'] <= stop_price).any(): - # Stop loss triggered, find the exact candle - stop_candle = min1_slice[min1_slice['low'] <= stop_price].iloc[0] - # More realistic fill: if open < stop, fill at open, else at stop - if stop_candle['open'] < stop_price: - sell_price = stop_candle['open'] - else: - sell_price = stop_price - if debug: - print(f"STOP LOSS triggered: entry={entry_price}, stop={stop_price}, sell_price={sell_price}, entry_time={entry_time}, stop_time={stop_candle.name}") - btc_to_sell = coin - fee_btc = btc_to_sell * transaction_cost - btc_after_fee = btc_to_sell - fee_btc - usd = btc_after_fee * sell_price - trade_log.append({ - 'type': 'STOP', - 'entry': entry_price, - 'exit': sell_price, - 'entry_time': entry_time, - 'exit_time': stop_candle.name, # Use index name instead of timestamp column - 'fee_btc': fee_btc - }) - coin = 0 - position = 0 - entry_price = 0 - current_trade_min1_start_idx = None - continue - - # Update the start index for next check - current_trade_min1_start_idx = current_min1_end_idx - - # Entry: only if not in position and signal changes to 1 - if position == 0 and prev_mt != 1 and curr_mt == 1: - # Buy at open, fee is charged in BTC (base currency) - gross_btc = usd / price_open - fee_btc = gross_btc * transaction_cost - coin = gross_btc - fee_btc - entry_price = price_open - entry_time = date - usd = 0 - position = 1 - current_trade_min1_start_idx = None # Will be set on first stop loss check - trade_log.append({ - 'type': 'BUY', - 'entry': entry_price, - 'exit': None, - 'entry_time': entry_time, - 'exit_time': None, - 'fee_btc': fee_btc - }) - - # Exit: only if in position and signal changes from 1 to -1 - elif position == 1 and prev_mt == 1 and curr_mt == -1: - # Sell at open, fee is charged in BTC (base currency) - btc_to_sell = coin - fee_btc = btc_to_sell * transaction_cost - btc_after_fee = btc_to_sell - fee_btc - usd = btc_after_fee * price_open - trade_log.append({ - 'type': 'SELL', - 'entry': entry_price, - 'exit': price_open, - 'entry_time': entry_time, - 'exit_time': date, - 'fee_btc': fee_btc - }) - coin = 0 - position = 0 - entry_price = 0 - current_trade_min1_start_idx = None - - # Track drawdown - balance = usd if position == 0 else coin * price_close - if balance > max_balance: - max_balance = balance - drawdown = (max_balance - balance) / max_balance - drawdowns.append(drawdown) - - # If still in position at end, sell at last close - if position == 1: - btc_to_sell = coin - fee_btc = btc_to_sell * transaction_cost - btc_after_fee = btc_to_sell - fee_btc - usd = btc_after_fee * df['close'].iloc[-1] - trade_log.append({ - 'type': 'EOD', - 'entry': entry_price, - 'exit': df['close'].iloc[-1], - 'entry_time': entry_time, - 'exit_time': df['timestamp'].iloc[-1], - 'fee_btc': fee_btc - }) - coin = 0 - position = 0 - entry_price = 0 - - # Calculate statistics - final_balance = usd - n_trades = len(trade_log) - wins = [1 for t in trade_log if t['exit'] is not None and t['exit'] > t['entry']] - win_rate = len(wins) / n_trades if n_trades > 0 else 0 - max_drawdown = max(drawdowns) if drawdowns else 0 - avg_trade = np.mean([t['exit']/t['entry']-1 for t in trade_log if t['exit'] is not None]) if trade_log else 0 - - trades = [] - total_fees_btc = 0.0 - total_fees_usd = 0.0 - for trade in trade_log: - if trade['exit'] is not None: - profit_pct = (trade['exit'] - trade['entry']) / trade['entry'] - else: - profit_pct = 0.0 - trades.append({ - 'entry_time': trade['entry_time'], - 'exit_time': trade['exit_time'], - 'entry': trade['entry'], - 'exit': trade['exit'], - 'profit_pct': profit_pct, - 'type': trade.get('type', 'SELL') - }) - # Sum up BTC fees and their USD equivalent (use exit price if available) - fee_btc = trade.get('fee_btc', 0.0) - total_fees_btc += fee_btc - if fee_btc and trade.get('exit') is not None: - total_fees_usd += fee_btc * trade['exit'] - - results = { - "initial_usd": initial_usd, - "final_usd": final_balance, - "n_trades": n_trades, - "win_rate": win_rate, - "max_drawdown": max_drawdown, - "avg_trade": avg_trade, - "trade_log": trade_log, - "trades": trades, - "total_fees_btc": total_fees_btc, - "total_fees_usd": total_fees_usd, - } - if n_trades > 0: - results["first_trade"] = { - "entry_time": trade_log[0]['entry_time'], - "entry": trade_log[0]['entry'] - } - results["last_trade"] = { - "exit_time": trade_log[-1]['exit_time'], - "exit": trade_log[-1]['exit'] - } - return results +import pandas as pd +import numpy as np +import logging +from scipy.signal import find_peaks +from matplotlib.patches import Rectangle +from scipy import stats +import concurrent.futures +from functools import partial +from functools import lru_cache +import matplotlib.pyplot as plt + +# Color configuration +# Plot colors +DARK_BG_COLOR = '#181C27' +LEGEND_BG_COLOR = '#333333' +TITLE_COLOR = 'white' +AXIS_LABEL_COLOR = 'white' + +# Candlestick colors +CANDLE_UP_COLOR = '#089981' # Green +CANDLE_DOWN_COLOR = '#F23645' # Red + +# Marker colors +MIN_COLOR = 'red' +MAX_COLOR = 'green' + +# Line style colors +MIN_LINE_STYLE = 'g--' # Green dashed +MAX_LINE_STYLE = 'r--' # Red dashed +SMA7_LINE_STYLE = 'y-' # Yellow solid +SMA15_LINE_STYLE = 'm-' # Magenta solid + +# SuperTrend colors +ST_COLOR_UP = 'g-' +ST_COLOR_DOWN = 'r-' + +# Cache the calculation results by function parameters +@lru_cache(maxsize=32) +def cached_supertrend_calculation(period, multiplier, data_tuple): + # Convert tuple back to numpy arrays + high = np.array(data_tuple[0]) + low = np.array(data_tuple[1]) + close = np.array(data_tuple[2]) + + # Calculate TR and ATR using vectorized operations + tr = np.zeros_like(close) + tr[0] = high[0] - low[0] + hc_range = np.abs(high[1:] - close[:-1]) + lc_range = np.abs(low[1:] - close[:-1]) + hl_range = high[1:] - low[1:] + tr[1:] = np.maximum.reduce([hl_range, hc_range, lc_range]) + + # Use numpy's exponential moving average + atr = np.zeros_like(tr) + atr[0] = tr[0] + multiplier_ema = 2.0 / (period + 1) + for i in range(1, len(tr)): + atr[i] = (tr[i] * multiplier_ema) + (atr[i-1] * (1 - multiplier_ema)) + + # Calculate bands + upper_band = np.zeros_like(close) + lower_band = np.zeros_like(close) + for i in range(len(close)): + hl_avg = (high[i] + low[i]) / 2 + upper_band[i] = hl_avg + (multiplier * atr[i]) + lower_band[i] = hl_avg - (multiplier * atr[i]) + + final_upper = np.zeros_like(close) + final_lower = np.zeros_like(close) + supertrend = np.zeros_like(close) + trend = np.zeros_like(close) + final_upper[0] = upper_band[0] + final_lower[0] = lower_band[0] + if close[0] <= upper_band[0]: + supertrend[0] = upper_band[0] + trend[0] = -1 + else: + supertrend[0] = lower_band[0] + trend[0] = 1 + for i in range(1, len(close)): + if (upper_band[i] < final_upper[i-1]) or (close[i-1] > final_upper[i-1]): + final_upper[i] = upper_band[i] + else: + final_upper[i] = final_upper[i-1] + if (lower_band[i] > final_lower[i-1]) or (close[i-1] < final_lower[i-1]): + final_lower[i] = lower_band[i] + else: + final_lower[i] = final_lower[i-1] + if supertrend[i-1] == final_upper[i-1] and close[i] <= final_upper[i]: + supertrend[i] = final_upper[i] + trend[i] = -1 + elif supertrend[i-1] == final_upper[i-1] and close[i] > final_upper[i]: + supertrend[i] = final_lower[i] + trend[i] = 1 + elif supertrend[i-1] == final_lower[i-1] and close[i] >= final_lower[i]: + supertrend[i] = final_lower[i] + trend[i] = 1 + elif supertrend[i-1] == final_lower[i-1] and close[i] < final_lower[i]: + supertrend[i] = final_upper[i] + trend[i] = -1 + return { + 'supertrend': supertrend, + 'trend': trend, + 'upper_band': final_upper, + 'lower_band': final_lower + } + +def calculate_supertrend_external(data, period, multiplier): + # Convert DataFrame columns to hashable tuples + high_tuple = tuple(data['high']) + low_tuple = tuple(data['low']) + close_tuple = tuple(data['close']) + + # Call the cached function + return cached_supertrend_calculation(period, multiplier, (high_tuple, low_tuple, close_tuple)) + +class TrendDetectorSimple: + def __init__(self, data, verbose=False, display=False): + """ + Initialize the TrendDetectorSimple class. + + Parameters: + - data: pandas DataFrame containing price data + - verbose: boolean, whether to display detailed logging information + - display: boolean, whether to enable display/plotting features + """ + + self.data = data + self.verbose = verbose + self.display = display + + # Only define display-related variables if display is True + if self.display: + # Plot style configuration + self.plot_style = 'dark_background' + self.bg_color = DARK_BG_COLOR + self.plot_size = (12, 8) + + # Candlestick configuration + self.candle_width = 0.6 + self.candle_up_color = CANDLE_UP_COLOR + self.candle_down_color = CANDLE_DOWN_COLOR + self.candle_alpha = 0.8 + self.wick_width = 1 + + # Marker configuration + self.min_marker = '^' + self.min_color = MIN_COLOR + self.min_size = 100 + self.max_marker = 'v' + self.max_color = MAX_COLOR + self.max_size = 100 + self.marker_zorder = 100 + + # Line configuration + self.line_width = 1 + self.min_line_style = MIN_LINE_STYLE + self.max_line_style = MAX_LINE_STYLE + self.sma7_line_style = SMA7_LINE_STYLE + self.sma15_line_style = SMA15_LINE_STYLE + + # Text configuration + self.title_size = 14 + self.title_color = TITLE_COLOR + self.axis_label_size = 12 + self.axis_label_color = AXIS_LABEL_COLOR + + # Legend configuration + self.legend_loc = 'best' + self.legend_bg_color = LEGEND_BG_COLOR + + # Configure logging + logging.basicConfig(level=logging.INFO if verbose else logging.WARNING, + format='%(asctime)s - %(levelname)s - %(message)s') + self.logger = logging.getLogger('TrendDetectorSimple') + + # Convert data to pandas DataFrame if it's not already + if not isinstance(self.data, pd.DataFrame): + if isinstance(self.data, list): + self.data = pd.DataFrame({'close': self.data}) + else: + raise ValueError("Data must be a pandas DataFrame or a list") + + def calculate_tr(self): + """ + Calculate True Range (TR) for the price data. + + True Range is the greatest of: + 1. Current high - current low + 2. |Current high - previous close| + 3. |Current low - previous close| + + Returns: + - Numpy array of TR values + """ + df = self.data.copy() + high = df['high'].values + low = df['low'].values + close = df['close'].values + + tr = np.zeros_like(close) + tr[0] = high[0] - low[0] # First TR is just the first day's range + + for i in range(1, len(close)): + # Current high - current low + hl_range = high[i] - low[i] + # |Current high - previous close| + hc_range = abs(high[i] - close[i-1]) + # |Current low - previous close| + lc_range = abs(low[i] - close[i-1]) + + # TR is the maximum of these three values + tr[i] = max(hl_range, hc_range, lc_range) + + return tr + + def calculate_atr(self, period=14): + """ + Calculate Average True Range (ATR) for the price data. + + ATR is the exponential moving average of the True Range over a specified period. + + Parameters: + - period: int, the period for the ATR calculation (default: 14) + + Returns: + - Numpy array of ATR values + """ + + tr = self.calculate_tr() + atr = np.zeros_like(tr) + + # First ATR value is just the first TR + atr[0] = tr[0] + + # Calculate exponential moving average (EMA) of TR + multiplier = 2.0 / (period + 1) + + for i in range(1, len(tr)): + atr[i] = (tr[i] * multiplier) + (atr[i-1] * (1 - multiplier)) + + return atr + + def detect_trends(self): + """ + Detect trends by identifying local minima and maxima in the price data + using scipy.signal.find_peaks. + + Parameters: + - prominence: float, required prominence of peaks (relative to the price range) + - width: int, required width of peaks in data points + + Returns: + - DataFrame with columns for timestamps, prices, and trend indicators + - Dictionary containing analysis results including linear regression, SMAs, and SuperTrend indicators + """ + df = self.data + # close_prices = df['close'].values + + # max_peaks, _ = find_peaks(close_prices) + # min_peaks, _ = find_peaks(-close_prices) + + # df['is_min'] = False + # df['is_max'] = False + + # for peak in max_peaks: + # df.at[peak, 'is_max'] = True + # for peak in min_peaks: + # df.at[peak, 'is_min'] = True + + # result = df[['timestamp', 'close', 'is_min', 'is_max']].copy() + + # Perform linear regression on min_peaks and max_peaks + # min_prices = df['close'].iloc[min_peaks].values + # max_prices = df['close'].iloc[max_peaks].values + + # Linear regression for min peaks if we have at least 2 points + # min_slope, min_intercept, min_r_value, _, _ = stats.linregress(min_peaks, min_prices) + # Linear regression for max peaks if we have at least 2 points + # max_slope, max_intercept, max_r_value, _, _ = stats.linregress(max_peaks, max_prices) + + # Calculate Simple Moving Averages (SMA) for 7 and 15 periods + # sma_7 = pd.Series(close_prices).rolling(window=7, min_periods=1).mean().values + # sma_15 = pd.Series(close_prices).rolling(window=15, min_periods=1).mean().values + + analysis_results = {} + # analysis_results['linear_regression'] = { + # 'min': { + # 'slope': min_slope, + # 'intercept': min_intercept, + # 'r_squared': min_r_value ** 2 + # }, + # 'max': { + # 'slope': max_slope, + # 'intercept': max_intercept, + # 'r_squared': max_r_value ** 2 + # } + # } + # analysis_results['sma'] = { + # '7': sma_7, + # '15': sma_15 + # } + + # Calculate SuperTrend indicators + supertrend_results_list = self._calculate_supertrend_indicators() + analysis_results['supertrend'] = supertrend_results_list + + return analysis_results + + def _calculate_supertrend_indicators(self): + """ + Calculate SuperTrend indicators with different parameter sets in parallel. + Returns: + - list, the SuperTrend results + """ + supertrend_params = [ + {"period": 12, "multiplier": 3.0, "color_up": ST_COLOR_UP, "color_down": ST_COLOR_DOWN}, + {"period": 10, "multiplier": 1.0, "color_up": ST_COLOR_UP, "color_down": ST_COLOR_DOWN}, + {"period": 11, "multiplier": 2.0, "color_up": ST_COLOR_UP, "color_down": ST_COLOR_DOWN} + ] + data = self.data.copy() + + # For just 3 calculations, direct calculation might be faster than process pool + results = [] + for p in supertrend_params: + result = calculate_supertrend_external(data, p["period"], p["multiplier"]) + results.append(result) + + supertrend_results_list = [] + for params, result in zip(supertrend_params, results): + supertrend_results_list.append({ + "results": result, + "params": params + }) + return supertrend_results_list + + def plot_trends(self, trend_data, analysis_results, view="both"): + """ + Plot the price data with detected trends using a candlestick chart. + Also plots SuperTrend indicators with three different parameter sets. + + Parameters: + - trend_data: DataFrame, the output from detect_trends() + - analysis_results: Dictionary containing analysis results from detect_trends() + - view: str, one of 'both', 'trend', 'supertrend'; determines which plot(s) to display + + Returns: + - None (displays the plot) + """ + if not self.display: + return # Do nothing if display is False + + plt.style.use(self.plot_style) + + if view == "both": + fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(self.plot_size[0]*2, self.plot_size[1])) + else: + fig, ax = plt.subplots(figsize=self.plot_size) + ax1 = ax2 = None + if view == "trend": + ax1 = ax + elif view == "supertrend": + ax2 = ax + + fig.patch.set_facecolor(self.bg_color) + if ax1: ax1.set_facecolor(self.bg_color) + if ax2: ax2.set_facecolor(self.bg_color) + + df = self.data.copy() + + if ax1: + self._plot_trend_analysis(ax1, df, trend_data, analysis_results) + + if ax2: + self._plot_supertrend_analysis(ax2, df, analysis_results['supertrend']) + + plt.tight_layout() + plt.show() + + def _plot_candlesticks(self, ax, df): + """ + Plot candlesticks on the given axis. + + Parameters: + - ax: matplotlib.axes.Axes, the axis to plot on + - df: pandas.DataFrame, the data to plot + """ + from matplotlib.patches import Rectangle + + for i in range(len(df)): + # Get OHLC values for this candle + open_val = df['open'].iloc[i] + close_val = df['close'].iloc[i] + high_val = df['high'].iloc[i] + low_val = df['low'].iloc[i] + + # Determine candle color + color = self.candle_up_color if close_val >= open_val else self.candle_down_color + + # Plot candle body + body_height = abs(close_val - open_val) + bottom = min(open_val, close_val) + rect = Rectangle((i - self.candle_width/2, bottom), self.candle_width, body_height, + color=color, alpha=self.candle_alpha) + ax.add_patch(rect) + + # Plot candle wicks + ax.plot([i, i], [low_val, high_val], color=color, linewidth=self.wick_width) + + def _plot_trend_analysis(self, ax, df, trend_data, analysis_results): + """ + Plot trend analysis on the given axis. + + Parameters: + - ax: matplotlib.axes.Axes, the axis to plot on + - df: pandas.DataFrame, the data to plot + - trend_data: pandas.DataFrame, the trend data + - analysis_results: dict, the analysis results + """ + # Draw candlesticks + self._plot_candlesticks(ax, df) + + # Plot minima and maxima points + self._plot_min_max_points(ax, df, trend_data) + + # Plot trend lines and moving averages + if analysis_results: + self._plot_trend_lines(ax, df, analysis_results) + + # Configure the subplot + self._configure_subplot(ax, 'Price Chart with Trend Analysis', len(df)) + + def _plot_min_max_points(self, ax, df, trend_data): + """ + Plot minimum and maximum points on the given axis. + + Parameters: + - ax: matplotlib.axes.Axes, the axis to plot on + - df: pandas.DataFrame, the data to plot + - trend_data: pandas.DataFrame, the trend data + """ + min_indices = trend_data.index[trend_data['is_min'] == True].tolist() + if min_indices: + min_y = [df['close'].iloc[i] for i in min_indices] + ax.scatter(min_indices, min_y, color=self.min_color, s=self.min_size, + marker=self.min_marker, label='Local Minima', zorder=self.marker_zorder) + + max_indices = trend_data.index[trend_data['is_max'] == True].tolist() + if max_indices: + max_y = [df['close'].iloc[i] for i in max_indices] + ax.scatter(max_indices, max_y, color=self.max_color, s=self.max_size, + marker=self.max_marker, label='Local Maxima', zorder=self.marker_zorder) + + def _plot_trend_lines(self, ax, df, analysis_results): + """ + Plot trend lines on the given axis. + + Parameters: + - ax: matplotlib.axes.Axes, the axis to plot on + - df: pandas.DataFrame, the data to plot + - analysis_results: dict, the analysis results + """ + x_vals = np.arange(len(df)) + + # Minima regression line (support) + min_slope = analysis_results['linear_regression']['min']['slope'] + min_intercept = analysis_results['linear_regression']['min']['intercept'] + min_line = min_slope * x_vals + min_intercept + ax.plot(x_vals, min_line, self.min_line_style, linewidth=self.line_width, + label='Minima Regression') + + # Maxima regression line (resistance) + max_slope = analysis_results['linear_regression']['max']['slope'] + max_intercept = analysis_results['linear_regression']['max']['intercept'] + max_line = max_slope * x_vals + max_intercept + ax.plot(x_vals, max_line, self.max_line_style, linewidth=self.line_width, + label='Maxima Regression') + + # SMA-7 line + sma_7 = analysis_results['sma']['7'] + ax.plot(x_vals, sma_7, self.sma7_line_style, linewidth=self.line_width, + label='SMA-7') + + # SMA-15 line + sma_15 = analysis_results['sma']['15'] + valid_idx_15 = ~np.isnan(sma_15) + ax.plot(x_vals[valid_idx_15], sma_15[valid_idx_15], self.sma15_line_style, + linewidth=self.line_width, label='SMA-15') + + def _configure_subplot(self, ax, title, data_length): + """ + Configure the subplot with title, labels, limits, and legend. + + Parameters: + - ax: matplotlib.axes.Axes, the axis to configure + - title: str, the title of the subplot + - data_length: int, the length of the data + """ + # Set title and labels + ax.set_title(title, fontsize=self.title_size, color=self.title_color) + ax.set_xlabel('Date', fontsize=self.axis_label_size, color=self.axis_label_color) + ax.set_ylabel('Price', fontsize=self.axis_label_size, color=self.axis_label_color) + + # Set appropriate x-axis limits + ax.set_xlim(-0.5, data_length - 0.5) + + # Add a legend + ax.legend(loc=self.legend_loc, facecolor=self.legend_bg_color) + + def _plot_supertrend_analysis(self, ax, df, supertrend_results_list=None): + """ + Plot SuperTrend analysis on the given axis. + + Parameters: + - ax: matplotlib.axes.Axes, the axis to plot on + - df: pandas.DataFrame, the data to plot + - supertrend_results_list: list, the SuperTrend results (optional) + """ + self._plot_candlesticks(ax, df) + self._plot_supertrend_lines(ax, df, supertrend_results_list, style='Both') + self._configure_subplot(ax, 'Multiple SuperTrend Indicators', len(df)) + + def _plot_supertrend_lines(self, ax, df, supertrend_results_list, style="Horizontal"): + """ + Plot SuperTrend lines on the given axis. + + Parameters: + - ax: matplotlib.axes.Axes, the axis to plot on + - df: pandas.DataFrame, the data to plot + - supertrend_results_list: list, the SuperTrend results + """ + x_vals = np.arange(len(df)) + + if style == 'Horizontal' or style == 'Both': + if len(supertrend_results_list) != 3: + raise ValueError("Expected exactly 3 SuperTrend results for meta calculation") + + trends = [st["results"]["trend"] for st in supertrend_results_list] + + band_height = 0.02 * (df["high"].max() - df["low"].min()) + y_base = df["low"].min() - band_height * 1.5 + + prev_color = None + for i in range(1, len(x_vals)): + t_vals = [t[i] for t in trends] + up_count = t_vals.count(1) + down_count = t_vals.count(-1) + + if down_count == 3: + color = "red" + elif down_count == 2 and up_count == 1: + color = "orange" + elif down_count == 1 and up_count == 2: + color = "yellow" + elif up_count == 3: + color = "green" + else: + continue # skip if unknown or inconsistent values + + ax.add_patch(Rectangle( + (x_vals[i-1], y_base), + 1, + band_height, + color=color, + linewidth=0, + alpha=0.6 + )) + # Draw a vertical line at the change of color + if prev_color and prev_color != color: + ax.axvline(x_vals[i-1], color="grey", alpha=0.3, linewidth=1) + prev_color = color + + ax.set_ylim(bottom=y_base - band_height * 0.5) + if style == 'Curves' or style == 'Both': + for st in supertrend_results_list: + params = st["params"] + results = st["results"] + supertrend = results["supertrend"] + trend = results["trend"] + + # Plot SuperTrend line with color based on trend + for i in range(1, len(x_vals)): + if trend[i] == 1: # Uptrend + ax.plot(x_vals[i-1:i+1], supertrend[i-1:i+1], params["color_up"], linewidth=self.line_width) + else: # Downtrend + ax.plot(x_vals[i-1:i+1], supertrend[i-1:i+1], params["color_down"], linewidth=self.line_width) + self._plot_metasupertrend_lines(ax, df, supertrend_results_list) + self._add_supertrend_legend(ax, supertrend_results_list) + + def _plot_metasupertrend_lines(self, ax, df, supertrend_results_list): + """ + Plot a Meta SuperTrend line where all individual SuperTrends agree on trend. + + Parameters: + - ax: matplotlib.axes.Axes, the axis to plot on + - df: pandas.DataFrame, the data to plot + - supertrend_results_list: list, each item contains SuperTrend 'results' and 'params' + """ + x_vals = np.arange(len(df)) + + if len(supertrend_results_list) != 3: + raise ValueError("Expected exactly 3 SuperTrend results for meta calculation") + + trends = [st["results"]["trend"] for st in supertrend_results_list] + supertrends = [st["results"]["supertrend"] for st in supertrend_results_list] + params = supertrend_results_list[0]["params"] # Use first config for styling + + trends_arr = np.stack(trends, axis=1) + meta_trend = np.where((trends_arr[:,0] == trends_arr[:,1]) & (trends_arr[:,1] == trends_arr[:,2]), trends_arr[:,0], 0) + + for i in range(1, len(x_vals)): + t1, t2, t3 = trends[0][i], trends[1][i], trends[2][i] + if t1 == t2 == t3: + meta_trend = t1 + # Average the 3 supertrend values + st_avg_prev = np.mean([s[i-1] for s in supertrends]) + st_avg_curr = np.mean([s[i] for s in supertrends]) + color = params["color_up"] if meta_trend == 1 else params["color_down"] + ax.plot(x_vals[i-1:i+1], [st_avg_prev, st_avg_curr], color, linewidth=self.line_width) + + def _add_supertrend_legend(self, ax, supertrend_results_list): + """ + Add SuperTrend legend entries to the given axis. + + Parameters: + - ax: matplotlib.axes.Axes, the axis to add legend entries to + - supertrend_results_list: list, the SuperTrend results + """ + for st in supertrend_results_list: + params = st["params"] + period = params["period"] + multiplier = params["multiplier"] + color_up = params["color_up"] + color_down = params["color_down"] + + ax.plot([], [], color_up, linewidth=self.line_width, + label=f'ST (P:{period}, M:{multiplier}) Up') + ax.plot([], [], color_down, linewidth=self.line_width, + label=f'ST (P:{period}, M:{multiplier}) Down') + + def backtest_meta_supertrend(self, min1_df, initial_usd=10000, stop_loss_pct=0.05, transaction_cost=0.001, debug=False): + """ + Backtest a simple strategy using the meta supertrend (all three supertrends agree). + Buys when meta supertrend is positive, sells when negative, applies a percentage stop loss. + + Parameters: + - min1_df: pandas DataFrame, 1-minute timeframe data for more accurate stop loss checking (optional) + - initial_usd: float, starting USD amount + - stop_loss_pct: float, stop loss as a fraction (e.g. 0.05 for 5%) + - transaction_cost: float, transaction cost as a fraction (e.g. 0.001 for 0.1%) + - debug: bool, whether to print debug info + """ + df = self.data.copy().reset_index(drop=True) + df['timestamp'] = pd.to_datetime(df['timestamp']) + + # Get meta supertrend (all three agree) + supertrend_results_list = self._calculate_supertrend_indicators() + trends = [st['results']['trend'] for st in supertrend_results_list] + trends_arr = np.stack(trends, axis=1) + meta_trend = np.where((trends_arr[:,0] == trends_arr[:,1]) & (trends_arr[:,1] == trends_arr[:,2]), + trends_arr[:,0], 0) + + position = 0 # 0 = no position, 1 = long + entry_price = 0 + usd = initial_usd + coin = 0 + trade_log = [] + max_balance = initial_usd + drawdowns = [] + trades = [] + entry_time = None + current_trade_min1_start_idx = None + + min1_df['timestamp'] = pd.to_datetime(min1_df.index) + + for i in range(1, len(df)): + if i % 100 == 0 and debug: + self.logger.debug(f"Progress: {i}/{len(df)} rows processed.") + + price_open = df['open'].iloc[i] + price_high = df['high'].iloc[i] + price_low = df['low'].iloc[i] + price_close = df['close'].iloc[i] + date = df['timestamp'].iloc[i] + prev_mt = meta_trend[i-1] + curr_mt = meta_trend[i] + + # Check stop loss if in position + if position == 1: + stop_price = entry_price * (1 - stop_loss_pct) + + if current_trade_min1_start_idx is None: + # First check after entry, find the entry point in 1-min data + current_trade_min1_start_idx = min1_df.index[min1_df.index >= entry_time][0] + + # Get the end index for current check + current_min1_end_idx = min1_df.index[min1_df.index <= date][-1] + + # Check all 1-minute candles in between for stop loss + min1_slice = min1_df.loc[current_trade_min1_start_idx:current_min1_end_idx] + if (min1_slice['low'] <= stop_price).any(): + # Stop loss triggered, find the exact candle + stop_candle = min1_slice[min1_slice['low'] <= stop_price].iloc[0] + # More realistic fill: if open < stop, fill at open, else at stop + if stop_candle['open'] < stop_price: + sell_price = stop_candle['open'] + else: + sell_price = stop_price + if debug: + print(f"STOP LOSS triggered: entry={entry_price}, stop={stop_price}, sell_price={sell_price}, entry_time={entry_time}, stop_time={stop_candle.name}") + btc_to_sell = coin + fee_btc = btc_to_sell * transaction_cost + btc_after_fee = btc_to_sell - fee_btc + usd = btc_after_fee * sell_price + trade_log.append({ + 'type': 'STOP', + 'entry': entry_price, + 'exit': sell_price, + 'entry_time': entry_time, + 'exit_time': stop_candle.name, # Use index name instead of timestamp column + 'fee_btc': fee_btc + }) + coin = 0 + position = 0 + entry_price = 0 + current_trade_min1_start_idx = None + continue + + # Update the start index for next check + current_trade_min1_start_idx = current_min1_end_idx + + # Entry: only if not in position and signal changes to 1 + if position == 0 and prev_mt != 1 and curr_mt == 1: + # Buy at open, fee is charged in BTC (base currency) + gross_btc = usd / price_open + fee_btc = gross_btc * transaction_cost + coin = gross_btc - fee_btc + entry_price = price_open + entry_time = date + usd = 0 + position = 1 + current_trade_min1_start_idx = None # Will be set on first stop loss check + trade_log.append({ + 'type': 'BUY', + 'entry': entry_price, + 'exit': None, + 'entry_time': entry_time, + 'exit_time': None, + 'fee_btc': fee_btc + }) + + # Exit: only if in position and signal changes from 1 to -1 + elif position == 1 and prev_mt == 1 and curr_mt == -1: + # Sell at open, fee is charged in BTC (base currency) + btc_to_sell = coin + fee_btc = btc_to_sell * transaction_cost + btc_after_fee = btc_to_sell - fee_btc + usd = btc_after_fee * price_open + trade_log.append({ + 'type': 'SELL', + 'entry': entry_price, + 'exit': price_open, + 'entry_time': entry_time, + 'exit_time': date, + 'fee_btc': fee_btc + }) + coin = 0 + position = 0 + entry_price = 0 + current_trade_min1_start_idx = None + + # Track drawdown + balance = usd if position == 0 else coin * price_close + if balance > max_balance: + max_balance = balance + drawdown = (max_balance - balance) / max_balance + drawdowns.append(drawdown) + + # If still in position at end, sell at last close + if position == 1: + btc_to_sell = coin + fee_btc = btc_to_sell * transaction_cost + btc_after_fee = btc_to_sell - fee_btc + usd = btc_after_fee * df['close'].iloc[-1] + trade_log.append({ + 'type': 'EOD', + 'entry': entry_price, + 'exit': df['close'].iloc[-1], + 'entry_time': entry_time, + 'exit_time': df['timestamp'].iloc[-1], + 'fee_btc': fee_btc + }) + coin = 0 + position = 0 + entry_price = 0 + + # Calculate statistics + final_balance = usd + n_trades = len(trade_log) + wins = [1 for t in trade_log if t['exit'] is not None and t['exit'] > t['entry']] + win_rate = len(wins) / n_trades if n_trades > 0 else 0 + max_drawdown = max(drawdowns) if drawdowns else 0 + avg_trade = np.mean([t['exit']/t['entry']-1 for t in trade_log if t['exit'] is not None]) if trade_log else 0 + + trades = [] + total_fees_btc = 0.0 + total_fees_usd = 0.0 + for trade in trade_log: + if trade['exit'] is not None: + profit_pct = (trade['exit'] - trade['entry']) / trade['entry'] + else: + profit_pct = 0.0 + trades.append({ + 'entry_time': trade['entry_time'], + 'exit_time': trade['exit_time'], + 'entry': trade['entry'], + 'exit': trade['exit'], + 'profit_pct': profit_pct, + 'type': trade.get('type', 'SELL') + }) + # Sum up BTC fees and their USD equivalent (use exit price if available) + fee_btc = trade.get('fee_btc', 0.0) + total_fees_btc += fee_btc + if fee_btc and trade.get('exit') is not None: + total_fees_usd += fee_btc * trade['exit'] + + results = { + "initial_usd": initial_usd, + "final_usd": final_balance, + "n_trades": n_trades, + "win_rate": win_rate, + "max_drawdown": max_drawdown, + "avg_trade": avg_trade, + "trade_log": trade_log, + "trades": trades, + "total_fees_btc": total_fees_btc, + "total_fees_usd": total_fees_usd, + } + if n_trades > 0: + results["first_trade"] = { + "entry_time": trade_log[0]['entry_time'], + "entry": trade_log[0]['entry'] + } + results["last_trade"] = { + "exit_time": trade_log[-1]['exit_time'], + "exit": trade_log[-1]['exit'] + } + return results \ No newline at end of file diff --git a/cycles/utils/__init__.py b/cycles/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/apply_taxes_to_file.py b/cycles/utils/apply_taxes_to_file.py similarity index 100% rename from utils/apply_taxes_to_file.py rename to cycles/utils/apply_taxes_to_file.py diff --git a/cycles/utils/gsheets.py b/cycles/utils/gsheets.py new file mode 100644 index 0000000..853f002 --- /dev/null +++ b/cycles/utils/gsheets.py @@ -0,0 +1,128 @@ +import threading +import time +import queue +from google.oauth2.service_account import Credentials +import gspread +import math +import numpy as np +from collections import defaultdict + + +class GSheetBatchPusher(threading.Thread): + + def __init__(self, queue, timestamp, spreadsheet_name, interval=60, logging=None): + super().__init__(daemon=True) + self.queue = queue + self.timestamp = timestamp + self.spreadsheet_name = spreadsheet_name + self.interval = interval + self._stop_event = threading.Event() + self.logging = logging + + def run(self): + while not self._stop_event.is_set(): + self.push_all() + time.sleep(self.interval) + # Final push on stop + self.push_all() + + def stop(self): + self._stop_event.set() + + def push_all(self): + batch_results = [] + batch_trades = [] + while True: + try: + results, trades = self.queue.get_nowait() + batch_results.extend(results) + batch_trades.extend(trades) + except queue.Empty: + break + + if batch_results or batch_trades: + self.write_results_per_combination_gsheet(batch_results, batch_trades, self.timestamp, self.spreadsheet_name) + + + def write_results_per_combination_gsheet(self, results_rows, trade_rows, timestamp, spreadsheet_name="GlimBit Backtest Results"): + scopes = [ + "https://www.googleapis.com/auth/spreadsheets", + "https://www.googleapis.com/auth/drive" + ] + creds = Credentials.from_service_account_file('credentials/service_account.json', scopes=scopes) + gc = gspread.authorize(creds) + sh = gc.open(spreadsheet_name) + + try: + worksheet = sh.worksheet("Results") + except gspread.exceptions.WorksheetNotFound: + worksheet = sh.add_worksheet(title="Results", rows="1000", cols="20") + + # Clear the worksheet before writing new results + worksheet.clear() + + # Updated fieldnames to match your data rows + fieldnames = [ + "timeframe", "stop_loss_pct", "n_trades", "n_stop_loss", "win_rate", + "max_drawdown", "avg_trade", "profit_ratio", "initial_usd", "final_usd" + ] + + def to_native(val): + if isinstance(val, (np.generic, np.ndarray)): + val = val.item() + if hasattr(val, 'isoformat'): + return val.isoformat() + # Handle inf, -inf, nan + if isinstance(val, float): + if math.isinf(val): + return "∞" if val > 0 else "-∞" + if math.isnan(val): + return "" + return val + + # Write header if sheet is empty + if len(worksheet.get_all_values()) == 0: + worksheet.append_row(fieldnames) + + for row in results_rows: + values = [to_native(row.get(field, "")) for field in fieldnames] + worksheet.append_row(values) + + trades_fieldnames = [ + "entry_time", "exit_time", "entry_price", "exit_price", "profit_pct", "type" + ] + trades_by_combo = defaultdict(list) + + for trade in trade_rows: + tf = trade.get("timeframe") + sl = trade.get("stop_loss_pct") + trades_by_combo[(tf, sl)].append(trade) + + for (tf, sl), trades in trades_by_combo.items(): + sl_percent = int(round(sl * 100)) + sheet_name = f"Trades_{tf}_ST{sl_percent}%" + + try: + trades_ws = sh.worksheet(sheet_name) + except gspread.exceptions.WorksheetNotFound: + trades_ws = sh.add_worksheet(title=sheet_name, rows="1000", cols="20") + + # Clear the trades worksheet before writing new trades + trades_ws.clear() + + if len(trades_ws.get_all_values()) == 0: + trades_ws.append_row(trades_fieldnames) + + for trade in trades: + trade_row = [to_native(trade.get(field, "")) for field in trades_fieldnames] + try: + trades_ws.append_row(trade_row) + except gspread.exceptions.APIError as e: + if '429' in str(e): + if self.logging is not None: + self.logging.warning(f"Google Sheets API quota exceeded (429). Please wait one minute. Will retry on next batch push. Sheet: {sheet_name}") + # Re-queue the failed batch for retry + self.queue.put((results_rows, trade_rows)) + return # Stop pushing for this batch, will retry next interval + else: + raise \ No newline at end of file diff --git a/cycles/utils/storage.py b/cycles/utils/storage.py new file mode 100644 index 0000000..300d8cc --- /dev/null +++ b/cycles/utils/storage.py @@ -0,0 +1,155 @@ +import os +import json +import pandas as pd +import csv +from collections import defaultdict + +RESULTS_DIR = "results" +DATA_DIR = "data" + +class Storage: + + """Storage class for storing and loading results and data""" + def __init__(self, logging=None, results_dir=RESULTS_DIR, data_dir=DATA_DIR): + + self.results_dir = results_dir + self.data_dir = data_dir + self.logging = logging + + # Create directories if they don't exist + os.makedirs(self.results_dir, exist_ok=True) + os.makedirs(self.data_dir, exist_ok=True) + + def load_data(self, file_path, start_date, stop_date): + """Load data with optimized dtypes and filtering, supporting CSV and JSON input + Args: + file_path: path to the data file + start_date: start date + stop_date: stop date + Returns: + pandas DataFrame + """ + # Determine file type + _, ext = os.path.splitext(file_path) + ext = ext.lower() + try: + if ext == ".json": + with open(os.path.join(self.data_dir, file_path), 'r') as f: + raw = json.load(f) + data = pd.DataFrame(raw["Data"]) + # Convert columns to lowercase + data.columns = data.columns.str.lower() + # Convert timestamp to datetime + data["timestamp"] = pd.to_datetime(data["timestamp"], unit="s") + # Filter by date range + data = data[(data["timestamp"] >= start_date) & (data["timestamp"] <= stop_date)] + if self.logging is not None: + self.logging.info(f"Data loaded from {file_path} for date range {start_date} to {stop_date}") + return data.set_index("timestamp") + else: + # Define optimized dtypes + dtypes = { + 'Open': 'float32', + 'High': 'float32', + 'Low': 'float32', + 'Close': 'float32', + 'Volume': 'float32' + } + # Read data with original capitalized column names + data = pd.read_csv(os.path.join(self.data_dir, file_path), dtype=dtypes) + # Convert timestamp to datetime + data['Timestamp'] = pd.to_datetime(data['Timestamp'], unit='s') + # Filter by date range + data = data[(data['Timestamp'] >= start_date) & (data['Timestamp'] <= stop_date)] + # Now convert column names to lowercase + data.columns = data.columns.str.lower() + if self.logging is not None: + self.logging.info(f"Data loaded from {file_path} for date range {start_date} to {stop_date}") + return data.set_index('timestamp') + except Exception as e: + if self.logging is not None: + self.logging.error(f"Error loading data from {file_path}: {e}") + return None + + def format_row(self, row): + """Format a row for a combined results CSV file + Args: + row: row to format + Returns: + formatted row + """ + + return { + "timeframe": row["timeframe"], + "stop_loss_pct": f"{row['stop_loss_pct']*100:.2f}%", + "n_trades": row["n_trades"], + "n_stop_loss": row["n_stop_loss"], + "win_rate": f"{row['win_rate']*100:.2f}%", + "max_drawdown": f"{row['max_drawdown']*100:.2f}%", + "avg_trade": f"{row['avg_trade']*100:.2f}%", + "profit_ratio": f"{row['profit_ratio']*100:.2f}%", + "final_usd": f"{row['final_usd']:.2f}", + } + + def write_results_chunk(self, filename, fieldnames, rows, write_header=False, initial_usd=None): + """Write a chunk of results to a CSV file + Args: + filename: filename to write to + fieldnames: list of fieldnames + rows: list of rows + write_header: whether to write the header + initial_usd: initial USD + """ + mode = 'w' if write_header else 'a' + + with open(filename, mode, newline="") as csvfile: + writer = csv.DictWriter(csvfile, fieldnames=fieldnames) + if write_header: + csvfile.write(f"# initial_usd: {initial_usd}\n") + writer.writeheader() + + for row in rows: + # Only keep keys that are in fieldnames + filtered_row = {k: v for k, v in row.items() if k in fieldnames} + writer.writerow(filtered_row) + + def write_results_combined(self, filename, fieldnames, rows): + """Write a combined results to a CSV file + Args: + filename: filename to write to + fieldnames: list of fieldnames + rows: list of rows + """ + fname = os.path.join(self.results_dir, filename) + with open(fname, "w", newline="") as csvfile: + writer = csv.DictWriter(csvfile, fieldnames=fieldnames, delimiter='\t') + writer.writeheader() + for row in rows: + writer.writerow(self.format_row(row)) + if self.logging is not None: + self.logging.info(f"Combined results written to {fname}") + + def write_trades(self, all_trade_rows, trades_fieldnames): + """Write trades to a CSV file + Args: + all_trade_rows: list of trade rows + trades_fieldnames: list of trade fieldnames + logging: logging object + """ + + trades_by_combo = defaultdict(list) + for trade in all_trade_rows: + tf = trade.get("timeframe") + sl = trade.get("stop_loss_pct") + trades_by_combo[(tf, sl)].append(trade) + + for (tf, sl), trades in trades_by_combo.items(): + sl_percent = int(round(sl * 100)) + trades_filename = os.path.join(self.results_dir, f"trades_{tf}_ST{sl_percent}pct.csv") + with open(trades_filename, "w", newline="") as csvfile: + writer = csv.DictWriter(csvfile, fieldnames=trades_fieldnames) + writer.writeheader() + for trade in trades: + writer.writerow({k: trade.get(k, "") for k in trades_fieldnames}) + if self.logging is not None: + self.logging.info(f"Trades written to {trades_filename}") \ No newline at end of file diff --git a/cycles/utils/system.py b/cycles/utils/system.py new file mode 100644 index 0000000..8f8a07c --- /dev/null +++ b/cycles/utils/system.py @@ -0,0 +1,19 @@ +import os +import psutil + +class SystemUtils: + + def __init__(self, logging=None): + self.logging = logging + + def get_optimal_workers(self): + """Determine optimal number of worker processes based on system resources""" + cpu_count = os.cpu_count() or 4 + memory_gb = psutil.virtual_memory().total / (1024**3) + # Heuristic: Use 75% of cores, but cap based on available memory + # Assume each worker needs ~2GB for large datasets + workers_by_memory = max(1, int(memory_gb / 2)) + workers_by_cpu = max(1, int(cpu_count * 0.75)) + if self.logging is not None: + self.logging.info(f"Using {min(workers_by_cpu, workers_by_memory)} workers for processing") + return min(workers_by_cpu, workers_by_memory) \ No newline at end of file diff --git a/main.py b/main.py index 51343d8..43dea81 100644 --- a/main.py +++ b/main.py @@ -1,21 +1,16 @@ import pandas as pd import numpy as np -from trend_detector_simple import TrendDetectorSimple -import csv import logging import concurrent.futures import os -import psutil import datetime -import gspread -from google.oauth2.service_account import Credentials -from collections import defaultdict -import threading import queue -import time -import math -import json -from taxes import Taxes + +from cycles.trend_detector_simple import TrendDetectorSimple +from cycles.taxes import Taxes +from cycles.utils.storage import Storage +from cycles.utils.gsheets import GSheetBatchPusher +from cycles.utils.system import SystemUtils # Set up logging logging.basicConfig( @@ -30,85 +25,6 @@ logging.basicConfig( # Global queue for batching Google Sheets updates results_queue = queue.Queue() -# Background thread function to push updates every minute -class GSheetBatchPusher(threading.Thread): - def __init__(self, queue, timestamp, spreadsheet_name, interval=60): - super().__init__(daemon=True) - self.queue = queue - self.timestamp = timestamp - self.spreadsheet_name = spreadsheet_name - self.interval = interval - self._stop_event = threading.Event() - - def run(self): - while not self._stop_event.is_set(): - self.push_all() - time.sleep(self.interval) - # Final push on stop - self.push_all() - - def stop(self): - self._stop_event.set() - - def push_all(self): - batch_results = [] - batch_trades = [] - while True: - try: - results, trades = self.queue.get_nowait() - batch_results.extend(results) - batch_trades.extend(trades) - except queue.Empty: - break - - if batch_results or batch_trades: - write_results_per_combination_gsheet(batch_results, batch_trades, self.timestamp, self.spreadsheet_name) - -def get_optimal_workers(): - """Determine optimal number of worker processes based on system resources""" - cpu_count = os.cpu_count() or 4 - memory_gb = psutil.virtual_memory().total / (1024**3) - # Heuristic: Use 75% of cores, but cap based on available memory - # Assume each worker needs ~2GB for large datasets - workers_by_memory = max(1, int(memory_gb / 2)) - workers_by_cpu = max(1, int(cpu_count * 0.75)) - return min(workers_by_cpu, workers_by_memory) - -def load_data(file_path, start_date, stop_date): - """Load data with optimized dtypes and filtering, supporting CSV and JSON input""" - # Determine file type - _, ext = os.path.splitext(file_path) - ext = ext.lower() - if ext == ".json": - with open(file_path, 'r') as f: - raw = json.load(f) - data = pd.DataFrame(raw["Data"]) - # Convert columns to lowercase - data.columns = data.columns.str.lower() - # Convert timestamp to datetime - data["timestamp"] = pd.to_datetime(data["timestamp"], unit="s") - # Filter by date range - data = data[(data["timestamp"] >= start_date) & (data["timestamp"] <= stop_date)] - return data.set_index("timestamp") - else: - # Define optimized dtypes - dtypes = { - 'Open': 'float32', - 'High': 'float32', - 'Low': 'float32', - 'Close': 'float32', - 'Volume': 'float32' - } - # Read data with original capitalized column names - data = pd.read_csv(file_path, dtype=dtypes) - # Convert timestamp to datetime - data['Timestamp'] = pd.to_datetime(data['Timestamp'], unit='s') - # Filter by date range - data = data[(data['Timestamp'] >= start_date) & (data['Timestamp'] <= stop_date)] - # Now convert column names to lowercase - data.columns = data.columns.str.lower() - return data.set_index('timestamp') - def process_timeframe_data(min1_df, df, stop_loss_pcts, rule_name, initial_usd, debug=False): """Process the entire timeframe with all stop loss values (no monthly split)""" df = df.copy().reset_index(drop=True) @@ -199,21 +115,6 @@ def process_timeframe(timeframe_info, debug=False): results_rows, all_trade_rows = process_timeframe_data(data_1min, df, [stop_loss_pct], rule, initial_usd, debug=debug) return results_rows, all_trade_rows -def write_results_chunk(filename, fieldnames, rows, write_header=False): - """Write a chunk of results to a CSV file""" - mode = 'w' if write_header else 'a' - - with open(filename, mode, newline="") as csvfile: - writer = csv.DictWriter(csvfile, fieldnames=fieldnames) - if write_header: - csvfile.write(f"# initial_usd: {initial_usd}\n") - writer.writeheader() - - for row in rows: - # Only keep keys that are in fieldnames - filtered_row = {k: v for k, v in row.items() if k in fieldnames} - writer.writerow(filtered_row) - def aggregate_results(all_rows): """Aggregate results per stop_loss_pct and per rule (timeframe)""" from collections import defaultdict @@ -250,87 +151,14 @@ def aggregate_results(all_rows): }) return summary_rows -def write_results_per_combination_gsheet(results_rows, trade_rows, timestamp, spreadsheet_name="GlimBit Backtest Results"): - scopes = [ - "https://www.googleapis.com/auth/spreadsheets", - "https://www.googleapis.com/auth/drive" - ] - creds = Credentials.from_service_account_file('credentials/service_account.json', scopes=scopes) - gc = gspread.authorize(creds) - sh = gc.open(spreadsheet_name) - - try: - worksheet = sh.worksheet("Results") - except gspread.exceptions.WorksheetNotFound: - worksheet = sh.add_worksheet(title="Results", rows="1000", cols="20") - - # Clear the worksheet before writing new results - worksheet.clear() - - # Updated fieldnames to match your data rows - fieldnames = [ - "timeframe", "stop_loss_pct", "n_trades", "n_stop_loss", "win_rate", - "max_drawdown", "avg_trade", "profit_ratio", "initial_usd", "final_usd" - ] - - def to_native(val): - if isinstance(val, (np.generic, np.ndarray)): - val = val.item() - if hasattr(val, 'isoformat'): - return val.isoformat() - # Handle inf, -inf, nan - if isinstance(val, float): - if math.isinf(val): - return "∞" if val > 0 else "-∞" - if math.isnan(val): - return "" - return val - - # Write header if sheet is empty - if len(worksheet.get_all_values()) == 0: - worksheet.append_row(fieldnames) - - for row in results_rows: - values = [to_native(row.get(field, "")) for field in fieldnames] - worksheet.append_row(values) - - trades_fieldnames = [ - "entry_time", "exit_time", "entry_price", "exit_price", "profit_pct", "type" - ] - trades_by_combo = defaultdict(list) - - for trade in trade_rows: - tf = trade.get("timeframe") - sl = trade.get("stop_loss_pct") - trades_by_combo[(tf, sl)].append(trade) - - for (tf, sl), trades in trades_by_combo.items(): - sl_percent = int(round(sl * 100)) - sheet_name = f"Trades_{tf}_ST{sl_percent}%" - - try: - trades_ws = sh.worksheet(sheet_name) - except gspread.exceptions.WorksheetNotFound: - trades_ws = sh.add_worksheet(title=sheet_name, rows="1000", cols="20") - - # Clear the trades worksheet before writing new trades - trades_ws.clear() - - if len(trades_ws.get_all_values()) == 0: - trades_ws.append_row(trades_fieldnames) - - for trade in trades: - trade_row = [to_native(trade.get(field, "")) for field in trades_fieldnames] - try: - trades_ws.append_row(trade_row) - except gspread.exceptions.APIError as e: - if '429' in str(e): - logging.warning(f"Google Sheets API quota exceeded (429). Please wait one minute. Will retry on next batch push. Sheet: {sheet_name}") - # Re-queue the failed batch for retry - results_queue.put((results_rows, trade_rows)) - return # Stop pushing for this batch, will retry next interval - else: - raise +def get_nearest_price(df, target_date): + if len(df) == 0: + return None, None + target_ts = pd.to_datetime(target_date) + nearest_idx = df.index.get_indexer([target_ts], method='nearest')[0] + nearest_time = df.index[nearest_idx] + price = df.iloc[nearest_idx]['close'] + return nearest_time, price if __name__ == "__main__": # Configuration @@ -341,24 +169,16 @@ if __name__ == "__main__": initial_usd = 10000 debug = False - results_dir = "results" - os.makedirs(results_dir, exist_ok=True) timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M") - timeframes = ["1min", "5min"] + storage = Storage(logging=logging) + system_utils = SystemUtils(logging=logging) + + timeframes = ["1D"] stop_loss_pcts = [0.01, 0.02, 0.03] # Load data once - data_1min = load_data('./data/btcusd_1-min_data.csv', start_date, stop_date) - - def get_nearest_price(df, target_date): - if len(df) == 0: - return None, None - target_ts = pd.to_datetime(target_date) - nearest_idx = df.index.get_indexer([target_ts], method='nearest')[0] - nearest_time = df.index[nearest_idx] - price = df.iloc[nearest_idx]['close'] - return nearest_time, price + data_1min = storage.load_data('btcusd_1-min_data.csv', start_date, stop_date) nearest_start_time, start_price = get_nearest_price(data_1min, start_date) nearest_stop_time, stop_price = get_nearest_price(data_1min, stop_date) @@ -372,8 +192,7 @@ if __name__ == "__main__": for stop_loss_pct in stop_loss_pcts ] - workers = get_optimal_workers() - logging.info(f"Using {workers} workers for processing") + workers = system_utils.get_optimal_workers() # Start the background batch pusher # spreadsheet_name = "GlimBit Backtest Results" @@ -403,33 +222,12 @@ if __name__ == "__main__": # batch_pusher.push_all() # Write all results to a single CSV file - combined_filename = os.path.join(results_dir, f"{timestamp}_backtest_combined.csv") + combined_filename = os.path.join(f"{timestamp}_backtest_combined.csv") combined_fieldnames = [ "timeframe", "stop_loss_pct", "n_trades", "n_stop_loss", "win_rate", "max_drawdown", "avg_trade", "profit_ratio", "final_usd" ] - - def format_row(row): - # Format percentages and floats as in your example - return { - "timeframe": row["timeframe"], - "stop_loss_pct": f"{row['stop_loss_pct']*100:.2f}%", - "n_trades": row["n_trades"], - "n_stop_loss": row["n_stop_loss"], - "win_rate": f"{row['win_rate']*100:.2f}%", - "max_drawdown": f"{row['max_drawdown']*100:.2f}%", - "avg_trade": f"{row['avg_trade']*100:.2f}%", - "profit_ratio": f"{row['profit_ratio']*100:.2f}%", - "final_usd": f"{row['final_usd']:.2f}", - } - - with open(combined_filename, "w", newline="") as csvfile: - writer = csv.DictWriter(csvfile, fieldnames=combined_fieldnames, delimiter='\t') - writer.writeheader() - for row in all_results_rows: - writer.writerow(format_row(row)) - - logging.info(f"Combined results written to {combined_filename}") + storage.write_results_combined(combined_filename, combined_fieldnames, all_results_rows) # --- Add taxes to combined results CSV --- # taxes = Taxes() # Default 20% tax rate @@ -457,25 +255,11 @@ if __name__ == "__main__": # --- END: Collect all trades from each task --- # Now, group all_trade_rows by (timeframe, stop_loss_pct) - from collections import defaultdict - trades_by_combo = defaultdict(list) - for trade in all_trade_rows: - tf = trade.get("timeframe") - sl = trade.get("stop_loss_pct") - trades_by_combo[(tf, sl)].append(trade) + trades_fieldnames = [ "entry_time", "exit_time", "entry_price", "exit_price", "profit_pct", "type" ] - - for (tf, sl), trades in trades_by_combo.items(): - sl_percent = int(round(sl * 100)) - trades_filename = os.path.join(results_dir, f"trades_{tf}_ST{sl_percent}pct.csv") - with open(trades_filename, "w", newline="") as csvfile: - writer = csv.DictWriter(csvfile, fieldnames=trades_fieldnames) - writer.writeheader() - for trade in trades: - writer.writerow({k: trade.get(k, "") for k in trades_fieldnames}) - logging.info(f"Trades written to {trades_filename}") + storage.write_trades(all_trade_rows, trades_fieldnames) \ No newline at end of file From 08c871e05a49fc8958c3650914a0e6fa6366e173 Mon Sep 17 00:00:00 2001 From: Ajasra Date: Tue, 20 May 2025 18:28:53 +0800 Subject: [PATCH 3/6] Boilinger Band and RSI implementation --- cycles/Analysis/__init__.py | 0 cycles/Analysis/boillinger_band.py | 50 +++++++++++ cycles/Analysis/rsi.py | 109 ++++++++++++++++++++++++ cycles/utils/data_utils.py | 60 +++++++++++++ cycles/utils/storage.py | 73 ++++++++++++++-- test_bbrsi.py | 132 +++++++++++++++++++++++++++++ 6 files changed, 415 insertions(+), 9 deletions(-) create mode 100644 cycles/Analysis/__init__.py create mode 100644 cycles/Analysis/boillinger_band.py create mode 100644 cycles/Analysis/rsi.py create mode 100644 cycles/utils/data_utils.py create mode 100644 test_bbrsi.py diff --git a/cycles/Analysis/__init__.py b/cycles/Analysis/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cycles/Analysis/boillinger_band.py b/cycles/Analysis/boillinger_band.py new file mode 100644 index 0000000..64d1e36 --- /dev/null +++ b/cycles/Analysis/boillinger_band.py @@ -0,0 +1,50 @@ +import pandas as pd + +class BollingerBands: + """ + Calculates Bollinger Bands for given financial data. + """ + def __init__(self, period: int = 20, std_dev_multiplier: float = 2.0): + """ + Initializes the BollingerBands calculator. + + Args: + period (int): The period for the moving average and standard deviation. + std_dev_multiplier (float): The number of standard deviations for the upper and lower bands. + """ + if period <= 0: + raise ValueError("Period must be a positive integer.") + if std_dev_multiplier <= 0: + raise ValueError("Standard deviation multiplier must be positive.") + + self.period = period + self.std_dev_multiplier = std_dev_multiplier + + def calculate(self, data_df: pd.DataFrame, price_column: str = 'close') -> pd.DataFrame: + """ + Calculates Bollinger Bands and adds them to the DataFrame. + + Args: + data_df (pd.DataFrame): DataFrame with price data. Must include the price_column. + price_column (str): The name of the column containing the price data (e.g., 'close'). + + Returns: + pd.DataFrame: The original DataFrame with added columns: + 'SMA' (Simple Moving Average), + 'UpperBand', + 'LowerBand'. + """ + if price_column not in data_df.columns: + raise ValueError(f"Price column '{price_column}' not found in DataFrame.") + + # Calculate SMA + data_df['SMA'] = data_df[price_column].rolling(window=self.period).mean() + + # Calculate Standard Deviation + std_dev = data_df[price_column].rolling(window=self.period).std() + + # Calculate Upper and Lower Bands + data_df['UpperBand'] = data_df['SMA'] + (self.std_dev_multiplier * std_dev) + data_df['LowerBand'] = data_df['SMA'] - (self.std_dev_multiplier * std_dev) + + return data_df diff --git a/cycles/Analysis/rsi.py b/cycles/Analysis/rsi.py new file mode 100644 index 0000000..a51c792 --- /dev/null +++ b/cycles/Analysis/rsi.py @@ -0,0 +1,109 @@ +import pandas as pd +import numpy as np + +class RSI: + """ + A class to calculate the Relative Strength Index (RSI). + """ + def __init__(self, period: int = 14): + """ + Initializes the RSI calculator. + + Args: + period (int): The period for RSI calculation. Default is 14. + Must be a positive integer. + """ + if not isinstance(period, int) or period <= 0: + raise ValueError("Period must be a positive integer.") + self.period = period + + def calculate(self, data_df: pd.DataFrame, price_column: str = 'close') -> pd.DataFrame: + """ + Calculates the RSI and adds it as a column to the input DataFrame. + + Args: + data_df (pd.DataFrame): DataFrame with historical price data. + Must contain the 'price_column'. + price_column (str): The name of the column containing price data. + Default is 'close'. + + Returns: + pd.DataFrame: The input DataFrame with an added 'RSI' column. + Returns the original DataFrame with no 'RSI' column + if the period is larger than the number of data points. + """ + if price_column not in data_df.columns: + raise ValueError(f"Price column '{price_column}' not found in DataFrame.") + + if len(data_df) < self.period: + print(f"Warning: Data length ({len(data_df)}) is less than RSI period ({self.period}). RSI will not be calculated.") + return data_df.copy() + + df = data_df.copy() + delta = df[price_column].diff(1) + + gain = delta.where(delta > 0, 0) + loss = -delta.where(delta < 0, 0) # Ensure loss is positive + + # Calculate initial average gain and loss (SMA) + avg_gain = gain.rolling(window=self.period, min_periods=self.period).mean().iloc[self.period -1:self.period] + avg_loss = loss.rolling(window=self.period, min_periods=self.period).mean().iloc[self.period -1:self.period] + + + # Calculate subsequent average gains and losses (EMA-like) + # Pre-allocate lists for gains and losses to avoid repeated appending to Series + gains = [0.0] * len(df) + losses = [0.0] * len(df) + + if not avg_gain.empty: + gains[self.period -1] = avg_gain.iloc[0] + if not avg_loss.empty: + losses[self.period -1] = avg_loss.iloc[0] + + + for i in range(self.period, len(df)): + gains[i] = ((gains[i-1] * (self.period - 1)) + gain.iloc[i]) / self.period + losses[i] = ((losses[i-1] * (self.period - 1)) + loss.iloc[i]) / self.period + + df['avg_gain'] = pd.Series(gains, index=df.index) + df['avg_loss'] = pd.Series(losses, index=df.index) + + # Calculate RS + # Handle division by zero: if avg_loss is 0, RS is undefined or infinite. + # If avg_loss is 0 and avg_gain is also 0, RSI is conventionally 50. + # If avg_loss is 0 and avg_gain > 0, RSI is conventionally 100. + rs = df['avg_gain'] / df['avg_loss'] + + # Calculate RSI + # RSI = 100 - (100 / (1 + RS)) + # If avg_loss is 0: + # If avg_gain > 0, RS -> inf, RSI -> 100 + # If avg_gain == 0, RS -> NaN (0/0), RSI -> 50 (conventionally, or could be 0 or 100 depending on interpretation) + # We will use a common convention where RSI is 100 if avg_loss is 0 and avg_gain > 0, + # and RSI is 0 if avg_loss is 0 and avg_gain is 0 (or 50, let's use 0 to indicate no strength if both are 0). + # However, to avoid NaN from 0/0, it's better to calculate RSI directly with conditions. + + rsi_values = [] + for i in range(len(df)): + avg_g = df['avg_gain'].iloc[i] + avg_l = df['avg_loss'].iloc[i] + + if i < self.period -1 : # Not enough data for initial SMA + rsi_values.append(np.nan) + continue + + if avg_l == 0: + if avg_g == 0: + rsi_values.append(50) # Or 0, or np.nan depending on how you want to treat this. 50 implies neutrality. + else: + rsi_values.append(100) # Max strength + else: + rs_val = avg_g / avg_l + rsi_values.append(100 - (100 / (1 + rs_val))) + + df['RSI'] = pd.Series(rsi_values, index=df.index) + + # Remove intermediate columns if desired, or keep them for debugging + # df.drop(columns=['avg_gain', 'avg_loss'], inplace=True) + + return df diff --git a/cycles/utils/data_utils.py b/cycles/utils/data_utils.py new file mode 100644 index 0000000..876bbe6 --- /dev/null +++ b/cycles/utils/data_utils.py @@ -0,0 +1,60 @@ +import pandas as pd + +def aggregate_to_daily(data_df: pd.DataFrame) -> pd.DataFrame: + """ + Aggregates time-series financial data to daily OHLCV format. + + The input DataFrame is expected to have a DatetimeIndex. + 'open' will be the first 'open' price of the day. + 'close' will be the last 'close' price of the day. + 'high' will be the maximum 'high' price of the day. + 'low' will be the minimum 'low' price of the day. + 'volume' (if present) will be the sum of volumes for the day. + + Args: + data_df (pd.DataFrame): DataFrame with a DatetimeIndex and columns + like 'open', 'high', 'low', 'close', and optionally 'volume'. + Column names are expected to be lowercase. + + Returns: + pd.DataFrame: DataFrame aggregated to daily OHLCV data. + The index will be a DatetimeIndex with the time set to noon (12:00:00) for each day. + Returns an empty DataFrame if no relevant OHLCV columns are found. + + Raises: + ValueError: If the input DataFrame does not have a DatetimeIndex. + """ + if not isinstance(data_df.index, pd.DatetimeIndex): + raise ValueError("Input DataFrame must have a DatetimeIndex.") + + agg_rules = {} + + # Define aggregation rules based on available columns + if 'open' in data_df.columns: + agg_rules['open'] = 'first' + if 'high' in data_df.columns: + agg_rules['high'] = 'max' + if 'low' in data_df.columns: + agg_rules['low'] = 'min' + if 'close' in data_df.columns: + agg_rules['close'] = 'last' + if 'volume' in data_df.columns: + agg_rules['volume'] = 'sum' + + if not agg_rules: + # Log a warning or raise an error if no relevant columns are found + # For now, returning an empty DataFrame with a message might be suitable for some cases + print("Warning: No standard OHLCV columns (open, high, low, close, volume) found for daily aggregation.") + return pd.DataFrame(index=pd.to_datetime([])) # Return empty DF with datetime index + + # Resample to daily frequency and apply aggregation rules + daily_data = data_df.resample('D').agg(agg_rules) + + # Adjust timestamps to noon if data exists + if not daily_data.empty and isinstance(daily_data.index, pd.DatetimeIndex): + daily_data.index = daily_data.index + pd.Timedelta(hours=12) + + # Remove rows where all values are NaN (these are days with no trades in the original data) + daily_data.dropna(how='all', inplace=True) + + return daily_data diff --git a/cycles/utils/storage.py b/cycles/utils/storage.py index 300d8cc..f202d2d 100644 --- a/cycles/utils/storage.py +++ b/cycles/utils/storage.py @@ -57,20 +57,75 @@ class Storage: } # Read data with original capitalized column names data = pd.read_csv(os.path.join(self.data_dir, file_path), dtype=dtypes) + + # Convert timestamp to datetime - data['Timestamp'] = pd.to_datetime(data['Timestamp'], unit='s') - # Filter by date range - data = data[(data['Timestamp'] >= start_date) & (data['Timestamp'] <= stop_date)] - # Now convert column names to lowercase - data.columns = data.columns.str.lower() - if self.logging is not None: - self.logging.info(f"Data loaded from {file_path} for date range {start_date} to {stop_date}") - return data.set_index('timestamp') + if 'Timestamp' in data.columns: + data['Timestamp'] = pd.to_datetime(data['Timestamp'], unit='s') + # Filter by date range + data = data[(data['Timestamp'] >= start_date) & (data['Timestamp'] <= stop_date)] + # Now convert column names to lowercase + data.columns = data.columns.str.lower() + if self.logging is not None: + self.logging.info(f"Data loaded from {file_path} for date range {start_date} to {stop_date}") + return data.set_index('timestamp') + else: # Attempt to use the first column if 'Timestamp' is not present + data.rename(columns={data.columns[0]: 'timestamp'}, inplace=True) + data['timestamp'] = pd.to_datetime(data['timestamp'], unit='s') + data = data[(data['timestamp'] >= start_date) & (data['timestamp'] <= stop_date)] + data.columns = data.columns.str.lower() # Ensure all other columns are lower + if self.logging is not None: + self.logging.info(f"Data loaded from {file_path} (using first column as timestamp) for date range {start_date} to {stop_date}") + return data.set_index('timestamp') except Exception as e: if self.logging is not None: self.logging.error(f"Error loading data from {file_path}: {e}") - return None + # Return an empty DataFrame with a DatetimeIndex + return pd.DataFrame(index=pd.to_datetime([])) + def save_data(self, data: pd.DataFrame, file_path: str): + """Save processed data to a CSV file. + If the DataFrame has a DatetimeIndex, it's converted to float Unix timestamps + (seconds since epoch) before saving. The index is saved as a column named 'timestamp'. + + Args: + data (pd.DataFrame): data to save. + file_path (str): path to the data file relative to the data_dir. + """ + data_to_save = data.copy() + + if isinstance(data_to_save.index, pd.DatetimeIndex): + # Convert DatetimeIndex to Unix timestamp (float seconds since epoch) + # and make it a column named 'timestamp'. + data_to_save['timestamp'] = data_to_save.index.astype('int64') / 1e9 + # Reset index so 'timestamp' column is saved and old DatetimeIndex is not saved as a column. + # We want the 'timestamp' column to be the first one. + data_to_save.reset_index(drop=True, inplace=True) + # Ensure 'timestamp' is the first column if other columns exist + if 'timestamp' in data_to_save.columns and len(data_to_save.columns) > 1: + cols = ['timestamp'] + [col for col in data_to_save.columns if col != 'timestamp'] + data_to_save = data_to_save[cols] + elif pd.api.types.is_numeric_dtype(data_to_save.index.dtype): + # If index is already numeric (e.g. float Unix timestamps from a previous save/load cycle), + # make it a column named 'timestamp'. + data_to_save['timestamp'] = data_to_save.index + data_to_save.reset_index(drop=True, inplace=True) + if 'timestamp' in data_to_save.columns and len(data_to_save.columns) > 1: + cols = ['timestamp'] + [col for col in data_to_save.columns if col != 'timestamp'] + data_to_save = data_to_save[cols] + else: + # For other index types, or if no index that we want to specifically handle, + # save with the current index. pandas to_csv will handle it. + # This branch might be removed if we strictly expect either DatetimeIndex or a numeric one from previous save. + pass # data_to_save remains as is, to_csv will write its index if index=True + + # Save to CSV, ensuring the 'timestamp' column (if created) is written, and not the DataFrame's active index. + full_path = os.path.join(self.data_dir, file_path) + data_to_save.to_csv(full_path, index=False) # index=False because timestamp is now a column + if self.logging is not None: + self.logging.info(f"Data saved to {full_path} with Unix timestamp column.") + + def format_row(self, row): """Format a row for a combined results CSV file Args: diff --git a/test_bbrsi.py b/test_bbrsi.py new file mode 100644 index 0000000..61af037 --- /dev/null +++ b/test_bbrsi.py @@ -0,0 +1,132 @@ +import logging +import seaborn as sns +import matplotlib.pyplot as plt +import pandas as pd + +from cycles.utils.storage import Storage +from cycles.utils.data_utils import aggregate_to_daily +from cycles.Analysis.boillinger_band import BollingerBands +from cycles.Analysis.rsi import RSI + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s", + handlers=[ + logging.FileHandler("backtest.log"), + logging.StreamHandler() + ] +) + +config_minute = { + "start_date": "2022-01-01", + "stop_date": "2023-01-01", + "data_file": "btcusd_1-min_data.csv" +} + +config_day = { + "start_date": "2022-01-01", + "stop_date": "2023-01-01", + "data_file": "btcusd_1-day_data.csv" +} + +IS_DAY = True + +def no_strategy(data_bb, data_with_rsi): + buy_condition = pd.Series([False] * len(data_bb), index=data_bb.index) + sell_condition = pd.Series([False] * len(data_bb), index=data_bb.index) + return buy_condition, sell_condition + +def strategy_1(data_bb, data_with_rsi): + # Long trade: price move below lower Bollinger band and RSI go below 25 + buy_condition = (data_bb['close'] < data_bb['LowerBand']) & (data_bb['RSI'] < 25) + # Short only: price move above top Bollinger band and RSI goes over 75 + sell_condition = (data_bb['close'] > data_bb['UpperBand']) & (data_bb['RSI'] > 75) + return buy_condition, sell_condition + + +if __name__ == "__main__": + + storage = Storage(logging=logging) + + if IS_DAY: + config = config_day + else: + config = config_minute + + data = storage.load_data(config["data_file"], config["start_date"], config["stop_date"]) + + if not IS_DAY: + data_daily = aggregate_to_daily(data) + storage.save_data(data, "btcusd_1-day_data.csv") + df_to_plot = data_daily + else: + df_to_plot = data + + bb = BollingerBands(period=30, std_dev_multiplier=2.0) + data_bb = bb.calculate(df_to_plot.copy()) + + rsi_calculator = RSI(period=13) + data_with_rsi = rsi_calculator.calculate(df_to_plot.copy(), price_column='close') + + # Combine BB and RSI data into a single DataFrame for signal generation + # Ensure indices are aligned; they should be as both are from df_to_plot.copy() + if 'RSI' in data_with_rsi.columns: + data_bb['RSI'] = data_with_rsi['RSI'] + else: + # If RSI wasn't calculated (e.g., not enough data), create a dummy column with NaNs + # to prevent errors later, though signals won't be generated. + data_bb['RSI'] = pd.Series(index=data_bb.index, dtype=float) + logging.warning("RSI column not found or not calculated. Signals relying on RSI may not be generated.") + + strategy = 1 + if strategy == 1: + buy_condition, sell_condition = strategy_1(data_bb, data_with_rsi) + else: + buy_condition, sell_condition = no_strategy(data_bb, data_with_rsi) + + buy_signals = data_bb[buy_condition] + sell_signals = data_bb[sell_condition] + + # plot the data with seaborn library + if df_to_plot is not None and not df_to_plot.empty: + # Create a figure with two subplots, sharing the x-axis + fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(16, 8), sharex=True) + + # Plot 1: Close Price and Bollinger Bands + sns.lineplot(x=data_bb.index, y='close', data=data_bb, label='Close Price', ax=ax1) + sns.lineplot(x=data_bb.index, y='UpperBand', data=data_bb, label='Upper Band (BB)', ax=ax1) + sns.lineplot(x=data_bb.index, y='LowerBand', data=data_bb, label='Lower Band (BB)', ax=ax1) + # Plot Buy/Sell signals on Price chart + if not buy_signals.empty: + ax1.scatter(buy_signals.index, buy_signals['close'], color='green', marker='o', s=20, label='Buy Signal', zorder=5) + if not sell_signals.empty: + ax1.scatter(sell_signals.index, sell_signals['close'], color='red', marker='o', s=20, label='Sell Signal', zorder=5) + ax1.set_title('Price and Bollinger Bands with Signals') + ax1.set_ylabel('Price') + ax1.legend() + ax1.grid(True) + + # Plot 2: RSI + if 'RSI' in data_bb.columns: # Check data_bb now as it should contain RSI + sns.lineplot(x=data_bb.index, y='RSI', data=data_bb, label='RSI (14)', ax=ax2, color='purple') + ax2.axhline(70, color='red', linestyle='--', linewidth=0.8, label='Overbought (70)') + ax2.axhline(30, color='green', linestyle='--', linewidth=0.8, label='Oversold (30)') + # Plot Buy/Sell signals on RSI chart + if not buy_signals.empty: + ax2.scatter(buy_signals.index, buy_signals['RSI'], color='green', marker='o', s=20, label='Buy Signal (RSI)', zorder=5) + if not sell_signals.empty: + ax2.scatter(sell_signals.index, sell_signals['RSI'], color='red', marker='o', s=20, label='Sell Signal (RSI)', zorder=5) + ax2.set_title('Relative Strength Index (RSI) with Signals') + ax2.set_ylabel('RSI Value') + ax2.set_ylim(0, 100) # RSI is typically bounded between 0 and 100 + ax2.legend() + ax2.grid(True) + else: + logging.info("RSI data not available for plotting.") + + plt.xlabel('Date') # Common X-axis label + fig.tight_layout() # Adjust layout to prevent overlapping titles/labels + plt.show() + else: + logging.info("No data to plot.") + From 369b3c1daf8cee5680dc62c06e924613679af4f4 Mon Sep 17 00:00:00 2001 From: Ajasra Date: Tue, 20 May 2025 18:29:12 +0800 Subject: [PATCH 4/6] added seaborn to plot ( a bit simpler and more features than matplotlib) --- pyproject.toml | 1 + uv.lock | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 8f0fb93..70885b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,4 +10,5 @@ dependencies = [ "pandas>=2.2.3", "psutil>=7.0.0", "scipy>=1.15.3", + "seaborn>=0.13.2", ] diff --git a/uv.lock b/uv.lock index 525bb99..05d405a 100644 --- a/uv.lock +++ b/uv.lock @@ -172,6 +172,7 @@ dependencies = [ { name = "pandas" }, { name = "psutil" }, { name = "scipy" }, + { name = "seaborn" }, ] [package.metadata] @@ -181,6 +182,7 @@ requires-dist = [ { name = "pandas", specifier = ">=2.2.3" }, { name = "psutil", specifier = ">=7.0.0" }, { name = "scipy", specifier = ">=1.15.3" }, + { name = "seaborn", specifier = ">=0.13.2" }, ] [[package]] @@ -779,6 +781,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" }, ] +[[package]] +name = "seaborn" +version = "0.13.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "matplotlib" }, + { name = "numpy" }, + { name = "pandas" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/59/a451d7420a77ab0b98f7affa3a1d78a313d2f7281a57afb1a34bae8ab412/seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7", size = 1457696, upload-time = "2024-01-25T13:21:52.551Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/11/00d3c3dfc25ad54e731d91449895a79e4bf2384dc3ac01809010ba88f6d5/seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987", size = 294914, upload-time = "2024-01-25T13:21:49.598Z" }, +] + [[package]] name = "six" version = "1.17.0" From 07b9824b69c08f18f402c62bd86e55ab2039039b Mon Sep 17 00:00:00 2001 From: Ajasra Date: Tue, 20 May 2025 18:36:59 +0800 Subject: [PATCH 5/6] docs --- docs/analysis.md | 51 ++++++++++++++++++++++++++++++ docs/utils_storage.md | 73 +++++++++++++++++++++++++++++++++++++++++++ docs/utils_system.md | 49 +++++++++++++++++++++++++++++ test_bbrsi.py | 4 +-- 4 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 docs/analysis.md create mode 100644 docs/utils_storage.md create mode 100644 docs/utils_system.md diff --git a/docs/analysis.md b/docs/analysis.md new file mode 100644 index 0000000..a4fc759 --- /dev/null +++ b/docs/analysis.md @@ -0,0 +1,51 @@ +# Analysis Module + +This document provides an overview of the `Analysis` module and its components, which are typically used for technical analysis of financial market data. + +## Modules + +The `Analysis` module includes classes for calculating common technical indicators: + +- **Relative Strength Index (RSI)**: Implemented in `cycles/Analysis/rsi.py`. +- **Bollinger Bands**: Implemented in `cycles/Analysis/boillinger_band.py`. + +## Class: `RSI` + +Found in `cycles/Analysis/rsi.py`. + +Calculates the Relative Strength Index. + +### `__init__(self, period: int = 14)` + +- **Description**: Initializes the RSI calculator. +- **Parameters**: + - `period` (int, optional): The period for RSI calculation. Defaults to 14. Must be a positive integer. + +### `calculate(self, data_df: pd.DataFrame, price_column: str = 'close') -> pd.DataFrame` + +- **Description**: Calculates the RSI and adds it as an 'RSI' column to the input DataFrame. Handles cases where data length is less than the period by returning the original DataFrame with a warning. +- **Parameters**: + - `data_df` (pd.DataFrame): DataFrame with historical price data. Must contain the `price_column`. + - `price_column` (str, optional): The name of the column containing price data. Defaults to 'close'. +- **Returns**: `pd.DataFrame` - The input DataFrame with an added 'RSI' column (containing `np.nan` for initial periods where RSI cannot be calculated). Returns a copy of the original DataFrame if the period is larger than the number of data points. + +## Class: `BollingerBands` + +Found in `cycles/Analysis/boillinger_band.py`. + +Calculates Bollinger Bands for given financial data. + +### `__init__(self, period: int = 20, std_dev_multiplier: float = 2.0)` + +- **Description**: Initializes the BollingerBands calculator. +- **Parameters**: + - `period` (int, optional): The period for the moving average and standard deviation. Defaults to 20. Must be a positive integer. + - `std_dev_multiplier` (float, optional): The number of standard deviations for the upper and lower bands. Defaults to 2.0. Must be positive. + +### `calculate(self, data_df: pd.DataFrame, price_column: str = 'close') -> pd.DataFrame` + +- **Description**: Calculates Bollinger Bands and adds 'SMA' (Simple Moving Average), 'UpperBand', and 'LowerBand' columns to the DataFrame. +- **Parameters**: + - `data_df` (pd.DataFrame): DataFrame with price data. Must include the `price_column`. + - `price_column` (str, optional): The name of the column containing the price data (e.g., 'close'). Defaults to 'close'. +- **Returns**: `pd.DataFrame` - The original DataFrame with added columns: 'SMA', 'UpperBand', 'LowerBand'. diff --git a/docs/utils_storage.md b/docs/utils_storage.md new file mode 100644 index 0000000..567c4e2 --- /dev/null +++ b/docs/utils_storage.md @@ -0,0 +1,73 @@ +# Storage Utilities + +This document describes the storage utility functions found in `cycles/utils/storage.py`. + +## Overview + +The `storage.py` module provides a `Storage` class designed for handling the loading and saving of data and results. It supports operations with CSV and JSON files and integrates with pandas DataFrames for data manipulation. The class also manages the creation of necessary `results` and `data` directories. + +## Constants + +- `RESULTS_DIR`: Defines the default directory name for storing results (default: "results"). +- `DATA_DIR`: Defines the default directory name for storing input data (default: "data"). + +## Class: `Storage` + +Handles storage operations for data and results. + +### `__init__(self, logging=None, results_dir=RESULTS_DIR, data_dir=DATA_DIR)` + +- **Description**: Initializes the `Storage` class. It creates the results and data directories if they don't already exist. +- **Parameters**: + - `logging` (optional): A logging instance for outputting information. Defaults to `None`. + - `results_dir` (str, optional): Path to the directory for storing results. Defaults to `RESULTS_DIR`. + - `data_dir` (str, optional): Path to the directory for storing data. Defaults to `DATA_DIR`. + +### `load_data(self, file_path, start_date, stop_date)` + +- **Description**: Loads data from a specified file (CSV or JSON), performs type optimization, filters by date range, and converts column names to lowercase. The timestamp column is set as the DataFrame index. +- **Parameters**: + - `file_path` (str): Path to the data file (relative to `data_dir`). + - `start_date` (datetime-like): The start date for filtering data. + - `stop_date` (datetime-like): The end date for filtering data. +- **Returns**: `pandas.DataFrame` - The loaded and processed data, with a `timestamp` index. Returns an empty DataFrame on error. + +### `save_data(self, data: pd.DataFrame, file_path: str)` + +- **Description**: Saves a pandas DataFrame to a CSV file within the `data_dir`. If the DataFrame has a DatetimeIndex, it's converted to a Unix timestamp (seconds since epoch) and stored in a column named 'timestamp', which becomes the first column in the CSV. The DataFrame's active index is not saved if a 'timestamp' column is created. +- **Parameters**: + - `data` (pd.DataFrame): The DataFrame to save. + - `file_path` (str): Path to the data file (relative to `data_dir`). + +### `format_row(self, row)` + +- **Description**: Formats a dictionary row for output to a combined results CSV file, applying specific string formatting for percentages and float values. +- **Parameters**: + - `row` (dict): The row of data to format. +- **Returns**: `dict` - The formatted row. + +### `write_results_chunk(self, filename, fieldnames, rows, write_header=False, initial_usd=None)` + +- **Description**: Writes a chunk of results (list of dictionaries) to a CSV file. Can append to an existing file or write a new one with a header. An optional `initial_usd` can be written as a comment in the header. +- **Parameters**: + - `filename` (str): The name of the file to write to (path is absolute or relative to current working dir). + - `fieldnames` (list): A list of strings representing the CSV header/column names. + - `rows` (list): A list of dictionaries, where each dictionary is a row. + - `write_header` (bool, optional): If `True`, writes the header. Defaults to `False`. + - `initial_usd` (numeric, optional): If provided and `write_header` is `True`, this value is written as a comment in the CSV header. Defaults to `None`. + +### `write_results_combined(self, filename, fieldnames, rows)` + +- **Description**: Writes combined results to a CSV file in the `results_dir`. Uses tab as a delimiter and formats rows using `format_row`. +- **Parameters**: + - `filename` (str): The name of the file to write to (relative to `results_dir`). + - `fieldnames` (list): A list of strings representing the CSV header/column names. + - `rows` (list): A list of dictionaries, where each dictionary is a row. + +### `write_trades(self, all_trade_rows, trades_fieldnames)` + +- **Description**: Writes trade data to separate CSV files based on timeframe and stop-loss percentage. Files are named `trades_{tf}_ST{sl_percent}pct.csv` and stored in `results_dir`. +- **Parameters**: + - `all_trade_rows` (list): A list of dictionaries, where each dictionary represents a trade. + - `trades_fieldnames` (list): A list of strings for the CSV header of trade files. + diff --git a/docs/utils_system.md b/docs/utils_system.md new file mode 100644 index 0000000..dc65acb --- /dev/null +++ b/docs/utils_system.md @@ -0,0 +1,49 @@ +# System Utilities + +This document describes the system utility functions found in `cycles/utils/system.py`. + +## Overview + +The `system.py` module provides utility functions related to system information and resource management. It currently includes a class `SystemUtils` for determining optimal configurations based on system resources. + +## Classes and Methods + +### `SystemUtils` + +A class to provide system-related utility methods. + +#### `__init__(self, logging=None)` + +- **Description**: Initializes the `SystemUtils` class. +- **Parameters**: + - `logging` (optional): A logging instance to output information. Defaults to `None`. + +#### `get_optimal_workers(self)` + +- **Description**: Determines the optimal number of worker processes based on available CPU cores and memory. + The heuristic aims to use 75% of CPU cores, with a cap based on available memory (assuming each worker might need ~2GB for large datasets). It returns the minimum of the workers calculated by CPU and memory. +- **Parameters**: None. +- **Returns**: `int` - The recommended number of worker processes. + +## Usage Examples + +```python +from cycles.utils.system import SystemUtils + +# Initialize (optionally with a logger) +# import logging +# logging.basicConfig(level=logging.INFO) +# logger = logging.getLogger(__name__) +# sys_utils = SystemUtils(logging=logger) +sys_utils = SystemUtils() + + +optimal_workers = sys_utils.get_optimal_workers() +print(f"Optimal number of workers: {optimal_workers}") + +# This value can then be used, for example, when setting up a ThreadPoolExecutor +# from concurrent.futures import ThreadPoolExecutor +# with ThreadPoolExecutor(max_workers=optimal_workers) as executor: +# # ... submit tasks ... +# pass +``` \ No newline at end of file diff --git a/test_bbrsi.py b/test_bbrsi.py index 61af037..93b5eb4 100644 --- a/test_bbrsi.py +++ b/test_bbrsi.py @@ -109,8 +109,8 @@ if __name__ == "__main__": # Plot 2: RSI if 'RSI' in data_bb.columns: # Check data_bb now as it should contain RSI sns.lineplot(x=data_bb.index, y='RSI', data=data_bb, label='RSI (14)', ax=ax2, color='purple') - ax2.axhline(70, color='red', linestyle='--', linewidth=0.8, label='Overbought (70)') - ax2.axhline(30, color='green', linestyle='--', linewidth=0.8, label='Oversold (30)') + ax2.axhline(75, color='red', linestyle='--', linewidth=0.8, label='Overbought (75)') + ax2.axhline(25, color='green', linestyle='--', linewidth=0.8, label='Oversold (25)') # Plot Buy/Sell signals on RSI chart if not buy_signals.empty: ax2.scatter(buy_signals.index, buy_signals['RSI'], color='green', marker='o', s=20, label='Buy Signal (RSI)', zorder=5) From 955a340d02f045a98992a75b5d7116bab0e604d9 Mon Sep 17 00:00:00 2001 From: Ajasra Date: Tue, 20 May 2025 18:36:59 +0800 Subject: [PATCH 6/6] docs --- docs/analysis.md | 78 +++++++++++++++++++++++++++++++++++++++++++ docs/utils_storage.md | 73 ++++++++++++++++++++++++++++++++++++++++ docs/utils_system.md | 49 +++++++++++++++++++++++++++ test_bbrsi.py | 4 +-- 4 files changed, 202 insertions(+), 2 deletions(-) create mode 100644 docs/analysis.md create mode 100644 docs/utils_storage.md create mode 100644 docs/utils_system.md diff --git a/docs/analysis.md b/docs/analysis.md new file mode 100644 index 0000000..159854d --- /dev/null +++ b/docs/analysis.md @@ -0,0 +1,78 @@ +# Analysis Module + +This document provides an overview of the `Analysis` module and its components, which are typically used for technical analysis of financial market data. + +## Modules + +The `Analysis` module includes classes for calculating common technical indicators: + +- **Relative Strength Index (RSI)**: Implemented in `cycles/Analysis/rsi.py`. +- **Bollinger Bands**: Implemented in `cycles/Analysis/boillinger_band.py`. + +## Class: `RSI` + +Found in `cycles/Analysis/rsi.py`. + +Calculates the Relative Strength Index. +### Mathematical Model +1. **Average Gain (AvgU)** and **Average Loss (AvgD)** over 14 periods: + $$ + \text{AvgU} = \frac{\sum \text{Upward Price Changes}}{14}, \quad \text{AvgD} = \frac{\sum \text{Downward Price Changes}}{14} + $$ +2. **Relative Strength (RS)**: + $$ + RS = \frac{\text{AvgU}}{\text{AvgD}} + $$ +3. **RSI**: + $$ + RSI = 100 - \frac{100}{1 + RS} + $$ + +### `__init__(self, period: int = 14)` + +- **Description**: Initializes the RSI calculator. +- **Parameters**: + - `period` (int, optional): The period for RSI calculation. Defaults to 14. Must be a positive integer. + +### `calculate(self, data_df: pd.DataFrame, price_column: str = 'close') -> pd.DataFrame` + +- **Description**: Calculates the RSI and adds it as an 'RSI' column to the input DataFrame. Handles cases where data length is less than the period by returning the original DataFrame with a warning. +- **Parameters**: + - `data_df` (pd.DataFrame): DataFrame with historical price data. Must contain the `price_column`. + - `price_column` (str, optional): The name of the column containing price data. Defaults to 'close'. +- **Returns**: `pd.DataFrame` - The input DataFrame with an added 'RSI' column (containing `np.nan` for initial periods where RSI cannot be calculated). Returns a copy of the original DataFrame if the period is larger than the number of data points. + +## Class: `BollingerBands` + +Found in `cycles/Analysis/boillinger_band.py`. + +## **Bollinger Bands** +### Mathematical Model +1. **Middle Band**: 20-day Simple Moving Average (SMA) + $$ + \text{Middle Band} = \frac{1}{20} \sum_{i=1}^{20} \text{Close}_{t-i} + $$ +2. **Upper Band**: Middle Band + 2 × 20-day Standard Deviation (σ) + $$ + \text{Upper Band} = \text{Middle Band} + 2 \times \sigma_{20} + $$ +3. **Lower Band**: Middle Band − 2 × 20-day Standard Deviation (σ) + $$ + \text{Lower Band} = \text{Middle Band} - 2 \times \sigma_{20} + $$ + + +### `__init__(self, period: int = 20, std_dev_multiplier: float = 2.0)` + +- **Description**: Initializes the BollingerBands calculator. +- **Parameters**: + - `period` (int, optional): The period for the moving average and standard deviation. Defaults to 20. Must be a positive integer. + - `std_dev_multiplier` (float, optional): The number of standard deviations for the upper and lower bands. Defaults to 2.0. Must be positive. + +### `calculate(self, data_df: pd.DataFrame, price_column: str = 'close') -> pd.DataFrame` + +- **Description**: Calculates Bollinger Bands and adds 'SMA' (Simple Moving Average), 'UpperBand', and 'LowerBand' columns to the DataFrame. +- **Parameters**: + - `data_df` (pd.DataFrame): DataFrame with price data. Must include the `price_column`. + - `price_column` (str, optional): The name of the column containing the price data (e.g., 'close'). Defaults to 'close'. +- **Returns**: `pd.DataFrame` - The original DataFrame with added columns: 'SMA', 'UpperBand', 'LowerBand'. diff --git a/docs/utils_storage.md b/docs/utils_storage.md new file mode 100644 index 0000000..567c4e2 --- /dev/null +++ b/docs/utils_storage.md @@ -0,0 +1,73 @@ +# Storage Utilities + +This document describes the storage utility functions found in `cycles/utils/storage.py`. + +## Overview + +The `storage.py` module provides a `Storage` class designed for handling the loading and saving of data and results. It supports operations with CSV and JSON files and integrates with pandas DataFrames for data manipulation. The class also manages the creation of necessary `results` and `data` directories. + +## Constants + +- `RESULTS_DIR`: Defines the default directory name for storing results (default: "results"). +- `DATA_DIR`: Defines the default directory name for storing input data (default: "data"). + +## Class: `Storage` + +Handles storage operations for data and results. + +### `__init__(self, logging=None, results_dir=RESULTS_DIR, data_dir=DATA_DIR)` + +- **Description**: Initializes the `Storage` class. It creates the results and data directories if they don't already exist. +- **Parameters**: + - `logging` (optional): A logging instance for outputting information. Defaults to `None`. + - `results_dir` (str, optional): Path to the directory for storing results. Defaults to `RESULTS_DIR`. + - `data_dir` (str, optional): Path to the directory for storing data. Defaults to `DATA_DIR`. + +### `load_data(self, file_path, start_date, stop_date)` + +- **Description**: Loads data from a specified file (CSV or JSON), performs type optimization, filters by date range, and converts column names to lowercase. The timestamp column is set as the DataFrame index. +- **Parameters**: + - `file_path` (str): Path to the data file (relative to `data_dir`). + - `start_date` (datetime-like): The start date for filtering data. + - `stop_date` (datetime-like): The end date for filtering data. +- **Returns**: `pandas.DataFrame` - The loaded and processed data, with a `timestamp` index. Returns an empty DataFrame on error. + +### `save_data(self, data: pd.DataFrame, file_path: str)` + +- **Description**: Saves a pandas DataFrame to a CSV file within the `data_dir`. If the DataFrame has a DatetimeIndex, it's converted to a Unix timestamp (seconds since epoch) and stored in a column named 'timestamp', which becomes the first column in the CSV. The DataFrame's active index is not saved if a 'timestamp' column is created. +- **Parameters**: + - `data` (pd.DataFrame): The DataFrame to save. + - `file_path` (str): Path to the data file (relative to `data_dir`). + +### `format_row(self, row)` + +- **Description**: Formats a dictionary row for output to a combined results CSV file, applying specific string formatting for percentages and float values. +- **Parameters**: + - `row` (dict): The row of data to format. +- **Returns**: `dict` - The formatted row. + +### `write_results_chunk(self, filename, fieldnames, rows, write_header=False, initial_usd=None)` + +- **Description**: Writes a chunk of results (list of dictionaries) to a CSV file. Can append to an existing file or write a new one with a header. An optional `initial_usd` can be written as a comment in the header. +- **Parameters**: + - `filename` (str): The name of the file to write to (path is absolute or relative to current working dir). + - `fieldnames` (list): A list of strings representing the CSV header/column names. + - `rows` (list): A list of dictionaries, where each dictionary is a row. + - `write_header` (bool, optional): If `True`, writes the header. Defaults to `False`. + - `initial_usd` (numeric, optional): If provided and `write_header` is `True`, this value is written as a comment in the CSV header. Defaults to `None`. + +### `write_results_combined(self, filename, fieldnames, rows)` + +- **Description**: Writes combined results to a CSV file in the `results_dir`. Uses tab as a delimiter and formats rows using `format_row`. +- **Parameters**: + - `filename` (str): The name of the file to write to (relative to `results_dir`). + - `fieldnames` (list): A list of strings representing the CSV header/column names. + - `rows` (list): A list of dictionaries, where each dictionary is a row. + +### `write_trades(self, all_trade_rows, trades_fieldnames)` + +- **Description**: Writes trade data to separate CSV files based on timeframe and stop-loss percentage. Files are named `trades_{tf}_ST{sl_percent}pct.csv` and stored in `results_dir`. +- **Parameters**: + - `all_trade_rows` (list): A list of dictionaries, where each dictionary represents a trade. + - `trades_fieldnames` (list): A list of strings for the CSV header of trade files. + diff --git a/docs/utils_system.md b/docs/utils_system.md new file mode 100644 index 0000000..dc65acb --- /dev/null +++ b/docs/utils_system.md @@ -0,0 +1,49 @@ +# System Utilities + +This document describes the system utility functions found in `cycles/utils/system.py`. + +## Overview + +The `system.py` module provides utility functions related to system information and resource management. It currently includes a class `SystemUtils` for determining optimal configurations based on system resources. + +## Classes and Methods + +### `SystemUtils` + +A class to provide system-related utility methods. + +#### `__init__(self, logging=None)` + +- **Description**: Initializes the `SystemUtils` class. +- **Parameters**: + - `logging` (optional): A logging instance to output information. Defaults to `None`. + +#### `get_optimal_workers(self)` + +- **Description**: Determines the optimal number of worker processes based on available CPU cores and memory. + The heuristic aims to use 75% of CPU cores, with a cap based on available memory (assuming each worker might need ~2GB for large datasets). It returns the minimum of the workers calculated by CPU and memory. +- **Parameters**: None. +- **Returns**: `int` - The recommended number of worker processes. + +## Usage Examples + +```python +from cycles.utils.system import SystemUtils + +# Initialize (optionally with a logger) +# import logging +# logging.basicConfig(level=logging.INFO) +# logger = logging.getLogger(__name__) +# sys_utils = SystemUtils(logging=logger) +sys_utils = SystemUtils() + + +optimal_workers = sys_utils.get_optimal_workers() +print(f"Optimal number of workers: {optimal_workers}") + +# This value can then be used, for example, when setting up a ThreadPoolExecutor +# from concurrent.futures import ThreadPoolExecutor +# with ThreadPoolExecutor(max_workers=optimal_workers) as executor: +# # ... submit tasks ... +# pass +``` \ No newline at end of file diff --git a/test_bbrsi.py b/test_bbrsi.py index 61af037..93b5eb4 100644 --- a/test_bbrsi.py +++ b/test_bbrsi.py @@ -109,8 +109,8 @@ if __name__ == "__main__": # Plot 2: RSI if 'RSI' in data_bb.columns: # Check data_bb now as it should contain RSI sns.lineplot(x=data_bb.index, y='RSI', data=data_bb, label='RSI (14)', ax=ax2, color='purple') - ax2.axhline(70, color='red', linestyle='--', linewidth=0.8, label='Overbought (70)') - ax2.axhline(30, color='green', linestyle='--', linewidth=0.8, label='Oversold (30)') + ax2.axhline(75, color='red', linestyle='--', linewidth=0.8, label='Overbought (75)') + ax2.axhline(25, color='green', linestyle='--', linewidth=0.8, label='Oversold (25)') # Plot Buy/Sell signals on RSI chart if not buy_signals.empty: ax2.scatter(buy_signals.index, buy_signals['RSI'], color='green', marker='o', s=20, label='Buy Signal (RSI)', zorder=5)