diff --git a/docker-compose.yml b/docker-compose.yml index 7f7e018..3c380d3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,6 +18,8 @@ services: ports: - 8081:8081 environment: + ME_CONFIG_BASICAUTH_USERNAME: root + ME_CONFIG_BASICAUTH_PASSWORD: root ME_CONFIG_MONGODB_ADMINUSERNAME: root ME_CONFIG_MONGODB_ADMINPASSWORD: root ME_CONFIG_MONGODB_URL: mongodb://root:root@mongo:27017/ \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 83cd73c..20b2d2c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,16 @@ # This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +[[package]] +name = "annotated-types" +version = "0.6.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, + {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, +] + [[package]] name = "appdirs" version = "1.4.4" @@ -1001,6 +1012,143 @@ files = [ {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, ] +[[package]] +name = "pydantic" +version = "2.4.2" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic-2.4.2-py3-none-any.whl", hash = "sha256:bc3ddf669d234f4220e6e1c4d96b061abe0998185a8d7855c0126782b7abc8c1"}, + {file = "pydantic-2.4.2.tar.gz", hash = "sha256:94f336138093a5d7f426aac732dcfe7ab4eb4da243c88f891d65deb4a2556ee7"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.10.1" +typing-extensions = ">=4.6.1" + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.10.1" +description = "" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic_core-2.10.1-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:d64728ee14e667ba27c66314b7d880b8eeb050e58ffc5fec3b7a109f8cddbd63"}, + {file = "pydantic_core-2.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:48525933fea744a3e7464c19bfede85df4aba79ce90c60b94d8b6e1eddd67096"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef337945bbd76cce390d1b2496ccf9f90b1c1242a3a7bc242ca4a9fc5993427a"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1392e0638af203cee360495fd2cfdd6054711f2db5175b6e9c3c461b76f5175"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0675ba5d22de54d07bccde38997e780044dcfa9a71aac9fd7d4d7a1d2e3e65f7"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:128552af70a64660f21cb0eb4876cbdadf1a1f9d5de820fed6421fa8de07c893"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f6e6aed5818c264412ac0598b581a002a9f050cb2637a84979859e70197aa9e"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ecaac27da855b8d73f92123e5f03612b04c5632fd0a476e469dfc47cd37d6b2e"}, + {file = "pydantic_core-2.10.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b3c01c2fb081fced3bbb3da78510693dc7121bb893a1f0f5f4b48013201f362e"}, + {file = "pydantic_core-2.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:92f675fefa977625105708492850bcbc1182bfc3e997f8eecb866d1927c98ae6"}, + {file = "pydantic_core-2.10.1-cp310-none-win32.whl", hash = "sha256:420a692b547736a8d8703c39ea935ab5d8f0d2573f8f123b0a294e49a73f214b"}, + {file = "pydantic_core-2.10.1-cp310-none-win_amd64.whl", hash = "sha256:0880e239827b4b5b3e2ce05e6b766a7414e5f5aedc4523be6b68cfbc7f61c5d0"}, + {file = "pydantic_core-2.10.1-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:073d4a470b195d2b2245d0343569aac7e979d3a0dcce6c7d2af6d8a920ad0bea"}, + {file = "pydantic_core-2.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:600d04a7b342363058b9190d4e929a8e2e715c5682a70cc37d5ded1e0dd370b4"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39215d809470f4c8d1881758575b2abfb80174a9e8daf8f33b1d4379357e417c"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eeb3d3d6b399ffe55f9a04e09e635554012f1980696d6b0aca3e6cf42a17a03b"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7a7902bf75779bc12ccfc508bfb7a4c47063f748ea3de87135d433a4cca7a2f"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3625578b6010c65964d177626fde80cf60d7f2e297d56b925cb5cdeda6e9925a"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:caa48fc31fc7243e50188197b5f0c4228956f97b954f76da157aae7f67269ae8"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:07ec6d7d929ae9c68f716195ce15e745b3e8fa122fc67698ac6498d802ed0fa4"}, + {file = "pydantic_core-2.10.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e6f31a17acede6a8cd1ae2d123ce04d8cca74056c9d456075f4f6f85de055607"}, + {file = "pydantic_core-2.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d8f1ebca515a03e5654f88411420fea6380fc841d1bea08effb28184e3d4899f"}, + {file = "pydantic_core-2.10.1-cp311-none-win32.whl", hash = "sha256:6db2eb9654a85ada248afa5a6db5ff1cf0f7b16043a6b070adc4a5be68c716d6"}, + {file = "pydantic_core-2.10.1-cp311-none-win_amd64.whl", hash = "sha256:4a5be350f922430997f240d25f8219f93b0c81e15f7b30b868b2fddfc2d05f27"}, + {file = "pydantic_core-2.10.1-cp311-none-win_arm64.whl", hash = "sha256:5fdb39f67c779b183b0c853cd6b45f7db84b84e0571b3ef1c89cdb1dfc367325"}, + {file = "pydantic_core-2.10.1-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:b1f22a9ab44de5f082216270552aa54259db20189e68fc12484873d926426921"}, + {file = "pydantic_core-2.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8572cadbf4cfa95fb4187775b5ade2eaa93511f07947b38f4cd67cf10783b118"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db9a28c063c7c00844ae42a80203eb6d2d6bbb97070cfa00194dff40e6f545ab"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2a35baa428181cb2270a15864ec6286822d3576f2ed0f4cd7f0c1708472aff"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05560ab976012bf40f25d5225a58bfa649bb897b87192a36c6fef1ab132540d7"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d6495008733c7521a89422d7a68efa0a0122c99a5861f06020ef5b1f51f9ba7c"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ac492c686defc8e6133e3a2d9eaf5261b3df26b8ae97450c1647286750b901"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8282bab177a9a3081fd3d0a0175a07a1e2bfb7fcbbd949519ea0980f8a07144d"}, + {file = "pydantic_core-2.10.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:aafdb89fdeb5fe165043896817eccd6434aee124d5ee9b354f92cd574ba5e78f"}, + {file = "pydantic_core-2.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f6defd966ca3b187ec6c366604e9296f585021d922e666b99c47e78738b5666c"}, + {file = "pydantic_core-2.10.1-cp312-none-win32.whl", hash = "sha256:7c4d1894fe112b0864c1fa75dffa045720a194b227bed12f4be7f6045b25209f"}, + {file = "pydantic_core-2.10.1-cp312-none-win_amd64.whl", hash = "sha256:5994985da903d0b8a08e4935c46ed8daf5be1cf217489e673910951dc533d430"}, + {file = "pydantic_core-2.10.1-cp312-none-win_arm64.whl", hash = "sha256:0d8a8adef23d86d8eceed3e32e9cca8879c7481c183f84ed1a8edc7df073af94"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:9badf8d45171d92387410b04639d73811b785b5161ecadabf056ea14d62d4ede"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:ebedb45b9feb7258fac0a268a3f6bec0a2ea4d9558f3d6f813f02ff3a6dc6698"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfe1090245c078720d250d19cb05d67e21a9cd7c257698ef139bc41cf6c27b4f"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e357571bb0efd65fd55f18db0a2fb0ed89d0bb1d41d906b138f088933ae618bb"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3dcd587b69bbf54fc04ca157c2323b8911033e827fffaecf0cafa5a892a0904"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c120c9ce3b163b985a3b966bb701114beb1da4b0468b9b236fc754783d85aa3"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15d6bca84ffc966cc9976b09a18cf9543ed4d4ecbd97e7086f9ce9327ea48891"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5cabb9710f09d5d2e9e2748c3e3e20d991a4c5f96ed8f1132518f54ab2967221"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:82f55187a5bebae7d81d35b1e9aaea5e169d44819789837cdd4720d768c55d15"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:1d40f55222b233e98e3921df7811c27567f0e1a4411b93d4c5c0f4ce131bc42f"}, + {file = "pydantic_core-2.10.1-cp37-none-win32.whl", hash = "sha256:14e09ff0b8fe6e46b93d36a878f6e4a3a98ba5303c76bb8e716f4878a3bee92c"}, + {file = "pydantic_core-2.10.1-cp37-none-win_amd64.whl", hash = "sha256:1396e81b83516b9d5c9e26a924fa69164156c148c717131f54f586485ac3c15e"}, + {file = "pydantic_core-2.10.1-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:6835451b57c1b467b95ffb03a38bb75b52fb4dc2762bb1d9dbed8de31ea7d0fc"}, + {file = "pydantic_core-2.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b00bc4619f60c853556b35f83731bd817f989cba3e97dc792bb8c97941b8053a"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fa467fd300a6f046bdb248d40cd015b21b7576c168a6bb20aa22e595c8ffcdd"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d99277877daf2efe074eae6338453a4ed54a2d93fb4678ddfe1209a0c93a2468"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa7db7558607afeccb33c0e4bf1c9a9a835e26599e76af6fe2fcea45904083a6"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aad7bd686363d1ce4ee930ad39f14e1673248373f4a9d74d2b9554f06199fb58"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:443fed67d33aa85357464f297e3d26e570267d1af6fef1c21ca50921d2976302"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:042462d8d6ba707fd3ce9649e7bf268633a41018d6a998fb5fbacb7e928a183e"}, + {file = "pydantic_core-2.10.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ecdbde46235f3d560b18be0cb706c8e8ad1b965e5c13bbba7450c86064e96561"}, + {file = "pydantic_core-2.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ed550ed05540c03f0e69e6d74ad58d026de61b9eaebebbaaf8873e585cbb18de"}, + {file = "pydantic_core-2.10.1-cp38-none-win32.whl", hash = "sha256:8cdbbd92154db2fec4ec973d45c565e767ddc20aa6dbaf50142676484cbff8ee"}, + {file = "pydantic_core-2.10.1-cp38-none-win_amd64.whl", hash = "sha256:9f6f3e2598604956480f6c8aa24a3384dbf6509fe995d97f6ca6103bb8c2534e"}, + {file = "pydantic_core-2.10.1-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:655f8f4c8d6a5963c9a0687793da37b9b681d9ad06f29438a3b2326d4e6b7970"}, + {file = "pydantic_core-2.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e570ffeb2170e116a5b17e83f19911020ac79d19c96f320cbfa1fa96b470185b"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64322bfa13e44c6c30c518729ef08fda6026b96d5c0be724b3c4ae4da939f875"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:485a91abe3a07c3a8d1e082ba29254eea3e2bb13cbbd4351ea4e5a21912cc9b0"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7c2b8eb9fc872e68b46eeaf835e86bccc3a58ba57d0eedc109cbb14177be531"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5cb87bdc2e5f620693148b5f8f842d293cae46c5f15a1b1bf7ceeed324a740c"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25bd966103890ccfa028841a8f30cebcf5875eeac8c4bde4fe221364c92f0c9a"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f323306d0556351735b54acbf82904fe30a27b6a7147153cbe6e19aaaa2aa429"}, + {file = "pydantic_core-2.10.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0c27f38dc4fbf07b358b2bc90edf35e82d1703e22ff2efa4af4ad5de1b3833e7"}, + {file = "pydantic_core-2.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f1365e032a477c1430cfe0cf2856679529a2331426f8081172c4a74186f1d595"}, + {file = "pydantic_core-2.10.1-cp39-none-win32.whl", hash = "sha256:a1c311fd06ab3b10805abb72109f01a134019739bd3286b8ae1bc2fc4e50c07a"}, + {file = "pydantic_core-2.10.1-cp39-none-win_amd64.whl", hash = "sha256:ae8a8843b11dc0b03b57b52793e391f0122e740de3df1474814c700d2622950a"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:d43002441932f9a9ea5d6f9efaa2e21458221a3a4b417a14027a1d530201ef1b"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fcb83175cc4936a5425dde3356f079ae03c0802bbdf8ff82c035f8a54b333521"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:962ed72424bf1f72334e2f1e61b68f16c0e596f024ca7ac5daf229f7c26e4208"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2cf5bb4dd67f20f3bbc1209ef572a259027c49e5ff694fa56bed62959b41e1f9"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e544246b859f17373bed915182ab841b80849ed9cf23f1f07b73b7c58baee5fb"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c0877239307b7e69d025b73774e88e86ce82f6ba6adf98f41069d5b0b78bd1bf"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:53df009d1e1ba40f696f8995683e067e3967101d4bb4ea6f667931b7d4a01357"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a1254357f7e4c82e77c348dabf2d55f1d14d19d91ff025004775e70a6ef40ada"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:524ff0ca3baea164d6d93a32c58ac79eca9f6cf713586fdc0adb66a8cdeab96a"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f0ac9fb8608dbc6eaf17956bf623c9119b4db7dbb511650910a82e261e6600f"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:320f14bd4542a04ab23747ff2c8a778bde727158b606e2661349557f0770711e"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:63974d168b6233b4ed6a0046296803cb13c56637a7b8106564ab575926572a55"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:417243bf599ba1f1fef2bb8c543ceb918676954734e2dcb82bf162ae9d7bd514"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:dda81e5ec82485155a19d9624cfcca9be88a405e2857354e5b089c2a982144b2"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:14cfbb00959259e15d684505263d5a21732b31248a5dd4941f73a3be233865b9"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:631cb7415225954fdcc2a024119101946793e5923f6c4d73a5914d27eb3d3a05"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:bec7dd208a4182e99c5b6c501ce0b1f49de2802448d4056091f8e630b28e9a52"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:149b8a07712f45b332faee1a2258d8ef1fb4a36f88c0c17cb687f205c5dc6e7d"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d966c47f9dd73c2d32a809d2be529112d509321c5310ebf54076812e6ecd884"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7eb037106f5c6b3b0b864ad226b0b7ab58157124161d48e4b30c4a43fef8bc4b"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:154ea7c52e32dce13065dbb20a4a6f0cc012b4f667ac90d648d36b12007fa9f7"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e562617a45b5a9da5be4abe72b971d4f00bf8555eb29bb91ec2ef2be348cd132"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:f23b55eb5464468f9e0e9a9935ce3ed2a870608d5f534025cd5536bca25b1402"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:e9121b4009339b0f751955baf4543a0bfd6bc3f8188f8056b1a25a2d45099934"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:0523aeb76e03f753b58be33b26540880bac5aa54422e4462404c432230543f33"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e0e2959ef5d5b8dc9ef21e1a305a21a36e254e6a34432d00c72a92fdc5ecda5"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da01bec0a26befab4898ed83b362993c844b9a607a86add78604186297eb047e"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f2e9072d71c1f6cfc79a36d4484c82823c560e6f5599c43c1ca6b5cdbd54f881"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f36a3489d9e28fe4b67be9992a23029c3cec0babc3bd9afb39f49844a8c721c5"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f64f82cc3443149292b32387086d02a6c7fb39b8781563e0ca7b8d7d9cf72bd7"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b4a6db486ac8e99ae696e09efc8b2b9fea67b63c8f88ba7a1a16c24a057a0776"}, + {file = "pydantic_core-2.10.1.tar.gz", hash = "sha256:0f8682dbdd2f67f8e1edddcbffcc29f60a6182b4901c367fc8c1c40d30bb0a82"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + [[package]] name = "pygments" version = "2.16.1" @@ -1654,4 +1802,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "be8c83a80c63c31f20f4f80c087f0795c83fc661cfc6bda0d0e5ce0e2a2fe712" +content-hash = "b948574efebb71c16f3e1b84465558767ca88f8e2c42afba8500c9dc81a164c0" diff --git a/pyproject.toml b/pyproject.toml index 4a3b849..9574641 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,8 +3,9 @@ name = "algorithmic-trader" description = "Trading bot with support for realtime trading, backtesting, custom strategies and much more" authors = ["Idan Yael"] maintainers = ["Idan Yael"] +packages = [{ include = "algotrader", from = "src" }] readme = "README.md" -version = "0.0.6.post1.dev0+7aabc15" +version = "0.0.0" keywords = ["algo-trader", "trading", "backtesting", "strategy", "bot"] license = "MIT" classifiers = [ @@ -38,7 +39,7 @@ target-version = ['py311'] [tool.poetry-dynamic-versioning] -enable = false +enable = true [tool.poetry.dependencies] python = ">=3.10,<3.13" @@ -52,6 +53,7 @@ coverage = "7.3.2" binance-connector = "1.18.0" python-dotenv = "0.21.0" ibapi = {path = "libs/ib_client"} +pydantic = "^2.4.2" [build-system] requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"] @@ -74,3 +76,23 @@ bug-tracker = "https://github.com/idanya/algo-trader/issues" [tool.pytest.ini_options] pythonpath = ["src"] + +[tool.pyright] +include = ["src/"] +exclude = ["**/node_modules", + "src/algotrader/providers/ib/", + "**/__pycache__", + "libs/", +] +defineConstant = { DEBUG = true } +venv = "env311" + +reportMissingImports = true +reportMissingTypeStubs = false + +pythonVersion = "3.11" +pythonPlatform = "Linux" + +executionEnvironments = [ + { root = "src" } +] \ No newline at end of file diff --git a/src/algotrader/entities/attachments/__init__.py b/src/algotrader/entities/attachments/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/algotrader/entities/attachments/assets_correlation.py b/src/algotrader/entities/attachments/assets_correlation.py new file mode 100644 index 0000000..6248b68 --- /dev/null +++ b/src/algotrader/entities/attachments/assets_correlation.py @@ -0,0 +1,8 @@ +from typing import Literal + +from algotrader.entities.attachments.technicals import IndicatorValue +from algotrader.entities.generic_candle_attachment import GenericCandleAttachment + + +class AssetCorrelation(GenericCandleAttachment[IndicatorValue]): + type: Literal["AssetCorrelation"] = "AssetCorrelation" diff --git a/src/algotrader/entities/attachments/nothing.py b/src/algotrader/entities/attachments/nothing.py new file mode 100644 index 0000000..692fb15 --- /dev/null +++ b/src/algotrader/entities/attachments/nothing.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +from typing import Literal + +from pydantic import Field + +from algotrader.entities.base_dto import BaseEntity + + +class NothingClass(BaseEntity): + type: Literal["NothingClass"] = "NothingClass" + nothing: str = Field(default="nothing-at-all") diff --git a/src/algotrader/entities/attachments/returns.py b/src/algotrader/entities/attachments/returns.py new file mode 100644 index 0000000..e4ef3b2 --- /dev/null +++ b/src/algotrader/entities/attachments/returns.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +from typing import Literal + +from algotrader.entities.generic_candle_attachment import GenericCandleAttachment + + +class Returns(GenericCandleAttachment[float]): + type: Literal["Returns"] = "Returns" diff --git a/src/algotrader/entities/attachments/technicals.py b/src/algotrader/entities/attachments/technicals.py new file mode 100644 index 0000000..970b63f --- /dev/null +++ b/src/algotrader/entities/attachments/technicals.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +from typing import Literal, Union, List + +from algotrader.entities.generic_candle_attachment import GenericCandleAttachment + +IndicatorValue = Union[List[float], float] + + +class Indicators(GenericCandleAttachment[IndicatorValue]): + type: Literal["Indicators"] = "Indicators" diff --git a/src/algotrader/entities/attachments/technicals_buckets_matcher.py b/src/algotrader/entities/attachments/technicals_buckets_matcher.py new file mode 100644 index 0000000..b9a642e --- /dev/null +++ b/src/algotrader/entities/attachments/technicals_buckets_matcher.py @@ -0,0 +1,8 @@ +from typing import Union, List, Literal + +from algotrader.entities.bucket import Bucket +from algotrader.entities.generic_candle_attachment import GenericCandleAttachment + + +class IndicatorsMatchedBuckets(GenericCandleAttachment[Union[List[Bucket], Bucket]]): + type: Literal["IndicatorsMatchedBuckets"] = "IndicatorsMatchedBuckets" diff --git a/src/algotrader/entities/attachments/technicals_normalizer.py b/src/algotrader/entities/attachments/technicals_normalizer.py new file mode 100644 index 0000000..655ffdc --- /dev/null +++ b/src/algotrader/entities/attachments/technicals_normalizer.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from typing import Literal + +from algotrader.entities.attachments.technicals import IndicatorValue +from algotrader.entities.generic_candle_attachment import GenericCandleAttachment + + +class NormalizedIndicators(GenericCandleAttachment[IndicatorValue]): + type: Literal["NormalizedIndicators"] = "NormalizedIndicators" diff --git a/src/algotrader/entities/base_dto.py b/src/algotrader/entities/base_dto.py new file mode 100644 index 0000000..aa773a0 --- /dev/null +++ b/src/algotrader/entities/base_dto.py @@ -0,0 +1,11 @@ +from datetime import datetime +from typing import ClassVar + +from pydantic import BaseModel, Field + + +class BaseEntity(BaseModel): + _types: ClassVar[dict[str, type]] = {} + + created_at: datetime = Field(default_factory=datetime.utcnow) + updated_at: datetime = Field(default_factory=datetime.utcnow) diff --git a/src/algotrader/entities/bucket.py b/src/algotrader/entities/bucket.py index f2fcda5..ec9a68d 100644 --- a/src/algotrader/entities/bucket.py +++ b/src/algotrader/entities/bucket.py @@ -1,29 +1,26 @@ from __future__ import annotations -from typing import Dict, List, Union +import math +from typing import List, Union, Optional -from algotrader.entities.serializable import Serializable, Deserializable +from pydantic import Field +from algotrader.entities.base_dto import BaseEntity -class Bucket(Serializable, Deserializable): - def __init__(self, ident: int, start: float = float("-inf"), end: float = float("inf")) -> None: - super().__init__() - self.ident = ident - self.start = start - self.end = end - @classmethod - def deserialize(cls, data: Dict) -> Bucket: - return Bucket(data["ident"], data["start"], data["end"]) +class Bucket(BaseEntity): + ident: float + start: Optional[float] = Field(default=-math.inf) + end: Optional[float] = Field(default=math.inf) - def serialize(self) -> Dict: - obj = super().serialize() - obj.update({"ident": self.ident, "start": self.start, "end": self.end}) + @property + def get_start(self): + return self.start or -math.inf - return obj + @property + def get_end(self): + return self.end or math.inf BucketList = List[Bucket] CompoundBucketList = Union[List[BucketList], BucketList] - -Bucket(0) diff --git a/src/algotrader/entities/bucketscontainer.py b/src/algotrader/entities/bucketscontainer.py index 316bab9..a75901e 100644 --- a/src/algotrader/entities/bucketscontainer.py +++ b/src/algotrader/entities/bucketscontainer.py @@ -2,15 +2,15 @@ from typing import Dict, Optional, ItemsView +from pydantic import Field + +from algotrader.entities.base_dto import BaseEntity from algotrader.entities.bucket import Bucket, CompoundBucketList -from algotrader.entities.serializable import Serializable, Deserializable from algotrader.serialization.store import DeserializationService -class BucketsContainer(Serializable, Deserializable): - def __init__(self) -> None: - super().__init__() - self.bins: Dict[str, CompoundBucketList] = {} +class BucketsContainer(BaseEntity): + bins: dict[str, CompoundBucketList] = Field(default_factory=dict) def items(self) -> ItemsView[str, CompoundBucketList]: return self.bins.items() diff --git a/src/algotrader/entities/candle.py b/src/algotrader/entities/candle.py index a17f0a0..183c92e 100644 --- a/src/algotrader/entities/candle.py +++ b/src/algotrader/entities/candle.py @@ -1,12 +1,13 @@ from __future__ import annotations from datetime import datetime -from typing import Dict, Optional +from typing import Optional, Annotated, Literal -from algotrader.entities.candle_attachments import CandleAttachments -from algotrader.entities.serializable import Serializable, Deserializable +from pydantic import Field + +from algotrader.entities.base_dto import BaseEntity +from algotrader.entities.candle_attachments import CandleAttachment from algotrader.entities.timespan import TimeSpan -from algotrader.serialization.store import DeserializationService def timestamp_to_str(d: datetime) -> str: @@ -17,60 +18,26 @@ def str_to_timestamp(d: str) -> datetime: return datetime.strptime(d, "%Y%m%d %H:%M:%S.%f") -class Candle(Serializable, Deserializable): - def __init__( - self, - symbol: str, - time_span: TimeSpan, - timestamp: datetime, - open: float, - close: float, - high: float, - low: float, - volume: float, - attachments: Optional[CandleAttachments] = None, - ): - self.symbol = symbol - self.timestamp = timestamp - self.time_span = time_span +class Candle(BaseEntity): + type: Literal["Candle"] = "Candle" + symbol: str + timestamp: datetime + time_span: TimeSpan + + open: float + close: float + high: float + low: float + volume: float - self.open = open - self.close = close - self.high = high - self.low = low - self.volume = volume - self.attachments = attachments or CandleAttachments() + attachments: Optional[dict[str, Annotated[CandleAttachment, Field(discriminator="type")]]] = None - def add_attachement(self, key: str, data: Serializable): - self.attachments.add_attachement(key, data) + def add_attachment(self, key: str, entity: BaseEntity): + if not self.attachments: + self.attachments = {} - def serialize(self) -> Dict: - obj = super().serialize() - obj.update( - { - "symbol": self.symbol, - "timestamp": timestamp_to_str(self.timestamp), - "timespan": self.time_span.value, - "open": self.open, - "close": self.close, - "high": self.high, - "low": self.low, - "volume": self.volume, - "attachments": self.attachments.serialize(), - } - ) - return obj + self.attachments[key] = entity - @classmethod - def deserialize(cls, data: Dict) -> Candle: - return cls( - data["symbol"], - TimeSpan(data["timespan"]), - str_to_timestamp(data["timestamp"]), - data["open"], - data["close"], - data["high"], - data["low"], - data["volume"], - DeserializationService.deserialize(data.get("attachments")), - ) + def get_attachment(self, key: str) -> Optional[CandleAttachment]: + if self.attachments: + return self.attachments.get(key) diff --git a/src/algotrader/entities/candle_attachments.py b/src/algotrader/entities/candle_attachments.py index fdd492e..8bf62a1 100644 --- a/src/algotrader/entities/candle_attachments.py +++ b/src/algotrader/entities/candle_attachments.py @@ -1,10 +1,17 @@ from __future__ import annotations -from typing import Dict, Optional +from typing import Dict, Optional, Union +from algotrader.entities.attachments.nothing import NothingClass from algotrader.entities.serializable import Serializable, Deserializable +from algotrader.entities.attachments.technicals_buckets_matcher import IndicatorsMatchedBuckets +from algotrader.entities.attachments.assets_correlation import AssetCorrelation +from algotrader.entities.attachments.technicals import Indicators +from algotrader.entities.attachments.technicals_normalizer import NormalizedIndicators from algotrader.serialization.store import DeserializationService +CandleAttachment = Union[NothingClass, NormalizedIndicators, Indicators, AssetCorrelation, IndicatorsMatchedBuckets] + class CandleAttachments(Serializable, Deserializable): def __init__(self) -> None: diff --git a/src/algotrader/entities/generic_candle_attachment.py b/src/algotrader/entities/generic_candle_attachment.py index a1873a5..9202433 100644 --- a/src/algotrader/entities/generic_candle_attachment.py +++ b/src/algotrader/entities/generic_candle_attachment.py @@ -1,55 +1,34 @@ from __future__ import annotations -from typing import Dict, TypeVar, Generic, Optional, ItemsView, Union +from typing import Dict, TypeVar, Generic, Optional, ItemsView -from algotrader.entities.serializable import Serializable, Deserializable +from pydantic import Field + +from algotrader.entities.base_dto import BaseEntity T = TypeVar("T") -class GenericCandleAttachment(Generic[T], Serializable, Deserializable): - def __init__(self) -> None: - super().__init__() - self._data: Dict[str, T] = {} +class GenericCandleAttachment(Generic[T], BaseEntity): + data: Dict[str, T] = Field(default_factory=dict) def __getitem__(self, key): - return self._data[key] + return self.data[key] def set(self, key: str, value: T): - self._data[key] = value + self.data[key] = value def get(self, key: str) -> Optional[T]: - return self._data[key] + return self.data[key] def items(self) -> ItemsView[str, T]: data = {} - for k, v in self._data.items(): + for k, v in self.data.items(): if k == "__class__": continue data.update({k: v}) return data.items() - @classmethod - def deserialize(cls, data: Dict) -> GenericCandleAttachment: - obj = GenericCandleAttachment() - obj._data = data - return obj - - def serialize(self) -> Dict: - obj = super().serialize() - for k, v in self._data.items(): - if v: - if isinstance(v, list): - obj[k] = [self._serialized_value(x) for x in v] - else: - obj[k] = self._serialized_value(v) - - return obj - - @staticmethod - def _serialized_value(value: Union[any, Serializable]): - return value.serialize() if isinstance(value, Serializable) else value - def has(self, key: str): - return key in self._data and self._data[key] is not None + return key in self.data and self.data[key] is not None diff --git a/src/algotrader/pipeline/pipeline.py b/src/algotrader/pipeline/pipeline.py index baee2f0..e0d9d6d 100644 --- a/src/algotrader/pipeline/pipeline.py +++ b/src/algotrader/pipeline/pipeline.py @@ -40,7 +40,7 @@ def run(self, context: SharedContext) -> None: self.logger.info("Starting pipeline...") for candle in self.source.read(): - self.logger.debug("Processing candle: %s\r", candle.serialize()) + self.logger.debug("Processing candle: %s\r", candle.model_dump()) self.processor.process(context, candle) if self.terminator: diff --git a/src/algotrader/pipeline/processors/__init__.py b/src/algotrader/pipeline/processors/__init__.py index f5b99f3..9b19583 100644 --- a/src/algotrader/pipeline/processors/__init__.py +++ b/src/algotrader/pipeline/processors/__init__.py @@ -5,7 +5,6 @@ from algotrader.pipeline.processors.storage_provider_sink import StorageSinkProcessor from algotrader.pipeline.processors.strategy import StrategyProcessor from algotrader.pipeline.processors.technicals import TechnicalsProcessor -from algotrader.pipeline.processors.technicals_buckets_matcher import IndicatorsMatchedBuckets from algotrader.pipeline.processors.technicals_normalizer import TechnicalsNormalizerProcessor from algotrader.pipeline.processors.timespan_change import TimeSpanChangeProcessor @@ -17,7 +16,6 @@ "StorageSinkProcessor", "StrategyProcessor", "TechnicalsProcessor", - "IndicatorsMatchedBuckets", "TechnicalsNormalizerProcessor", "TimeSpanChangeProcessor", ] diff --git a/src/algotrader/pipeline/processors/assets_correlation.py b/src/algotrader/pipeline/processors/assets_correlation.py index 5e2d12e..3f7e398 100644 --- a/src/algotrader/pipeline/processors/assets_correlation.py +++ b/src/algotrader/pipeline/processors/assets_correlation.py @@ -3,25 +3,17 @@ from scipy import spatial +from algotrader.entities.attachments.assets_correlation import AssetCorrelation from algotrader.entities.candle import Candle from algotrader.entities.event import Event -from algotrader.entities.generic_candle_attachment import GenericCandleAttachment from algotrader.pipeline.processor import Processor from algotrader.pipeline.processors.candle_cache import CandleCache -from algotrader.pipeline.processors.technicals import IndicatorValue from algotrader.pipeline.shared_context import SharedContext CORRELATIONS_ATTACHMENT_KEY = "correlations" CORRELATION_ELEMENTS_COUNT = 4 -class AssetCorrelation(GenericCandleAttachment[IndicatorValue]): - pass - - -AssetCorrelation() - - class CorrelationConfig: def __init__(self, groups: List[List[str]]) -> None: self.groups: List[List[str]] = groups @@ -88,7 +80,7 @@ def _calculate_symbol_correlations(self, context: SharedContext, symbol: str): asset_correlation.set(paired_symbol, correlation) latest_candle = current_symbol_candles[-1] - latest_candle.attachments.add_attachement(CORRELATIONS_ATTACHMENT_KEY, asset_correlation) + latest_candle.add_attachment(CORRELATIONS_ATTACHMENT_KEY, asset_correlation) self.reprocess(context, latest_candle) diff --git a/src/algotrader/pipeline/processors/file_sink.py b/src/algotrader/pipeline/processors/file_sink.py index 64aee4b..3fe9e05 100644 --- a/src/algotrader/pipeline/processors/file_sink.py +++ b/src/algotrader/pipeline/processors/file_sink.py @@ -26,4 +26,4 @@ def process(self, context: SharedContext, candle: Candle): super().process(context, candle) def _generate_candle_output(self, context: SharedContext, candle: Candle) -> str: - return json.dumps(candle.serialize()) + return json.dumps(candle.model_dump_json()) diff --git a/src/algotrader/pipeline/processors/returns.py b/src/algotrader/pipeline/processors/returns.py index 4bef17e..264f64d 100644 --- a/src/algotrader/pipeline/processors/returns.py +++ b/src/algotrader/pipeline/processors/returns.py @@ -2,8 +2,8 @@ from typing import List, Dict, Optional +from algotrader.entities.attachments.returns import Returns from algotrader.entities.candle import Candle -from algotrader.entities.generic_candle_attachment import GenericCandleAttachment from algotrader.pipeline.processor import Processor from algotrader.pipeline.processors.candle_cache import CandleCache from algotrader.pipeline.shared_context import SharedContext @@ -11,13 +11,6 @@ RETURNS_ATTACHMENT_KEY = "returns" -class Returns(GenericCandleAttachment[float]): - pass - - -Returns() - - class ReturnsCalculatorProcessor(Processor): def __init__(self, field_prefix: str, returns_count: int, next_processor: Optional[Processor] = None): super().__init__(next_processor) @@ -28,11 +21,11 @@ def process(self, context: SharedContext, candle: Candle): cache_reader = CandleCache.context_reader(context) symbol_candles = cache_reader.get_symbol_candles(candle.symbol) or [] - candle.attachments.add_attachement(RETURNS_ATTACHMENT_KEY, Returns()) + candle.add_attachment(RETURNS_ATTACHMENT_KEY, Returns()) if len(symbol_candles) >= self.returns_count: candle_returns = self._calc_returns(candle, symbol_candles) - candle.attachments.add_attachement(RETURNS_ATTACHMENT_KEY, candle_returns) + candle.add_attachment(RETURNS_ATTACHMENT_KEY, candle_returns) if self.next_processor: self.next_processor.process(context, candle) diff --git a/src/algotrader/pipeline/processors/technicals.py b/src/algotrader/pipeline/processors/technicals.py index 0615273..ccd9d74 100644 --- a/src/algotrader/pipeline/processors/technicals.py +++ b/src/algotrader/pipeline/processors/technicals.py @@ -3,8 +3,8 @@ from typing import Optional, List, Dict, Union, Tuple from algotrader.calc.technicals import TechnicalCalculator +from algotrader.entities.attachments.technicals import Indicators, IndicatorValue from algotrader.entities.candle import Candle -from algotrader.entities.generic_candle_attachment import GenericCandleAttachment from algotrader.pipeline.configs.technical_processor_config import TechnicalsProcessorConfig from algotrader.pipeline.processor import Processor from algotrader.pipeline.processors.candle_cache import CandleCache @@ -13,14 +13,6 @@ INDICATORS_ATTACHMENT_KEY = "indicators" TechnicalsData = Dict[str, Dict[str, List[float]]] -IndicatorValue = Union[List[float], float] - - -class Indicators(GenericCandleAttachment[IndicatorValue]): - pass - - -Indicators() MAX_CANDLES_FOR_CALC = 50 @@ -42,7 +34,7 @@ def process(self, context: SharedContext, candle: Candle): candle_indicators = Indicators() self._calculate(calculator, candle_indicators) - candle.attachments.add_attachement(INDICATORS_ATTACHMENT_KEY, candle_indicators) + candle.add_attachment(INDICATORS_ATTACHMENT_KEY, candle_indicators) super().process(context, candle) diff --git a/src/algotrader/pipeline/processors/technicals_buckets_matcher.py b/src/algotrader/pipeline/processors/technicals_buckets_matcher.py index 122f20a..11c28ad 100644 --- a/src/algotrader/pipeline/processors/technicals_buckets_matcher.py +++ b/src/algotrader/pipeline/processors/technicals_buckets_matcher.py @@ -1,41 +1,19 @@ -import json -from typing import Optional, List, Union, Dict +from typing import Optional, List, Dict +from algotrader.entities.attachments.technicals_buckets_matcher import IndicatorsMatchedBuckets from algotrader.entities.bucket import Bucket, BucketList from algotrader.entities.bucketscontainer import BucketsContainer from algotrader.entities.candle import Candle -from algotrader.entities.generic_candle_attachment import GenericCandleAttachment from algotrader.pipeline.processor import Processor from algotrader.pipeline.processors.technicals_normalizer import ( - NormalizedIndicators, NORMALIZED_INDICATORS_ATTACHMENT_KEY, ) +from algotrader.entities.attachments.technicals_normalizer import NormalizedIndicators from algotrader.pipeline.shared_context import SharedContext -from algotrader.serialization.store import DeserializationService INDICATORS_MATCHED_BUCKETS_ATTACHMENT_KEY = "indicators_matched_buckets" -class IndicatorsMatchedBuckets(GenericCandleAttachment[Union[List[Bucket], Bucket]]): - @classmethod - def deserialize(cls, data: Dict) -> GenericCandleAttachment: - obj = IndicatorsMatchedBuckets() - obj._data = data - for k, v in obj._data.items(): - if k == "__class__": - continue - - if isinstance(v, dict) and "__class__" in v: - obj._data[k] = DeserializationService.deserialize(v) - elif isinstance(v, list): - obj._data[k] = [DeserializationService.deserialize(item) for item in v] - - return obj - - -IndicatorsMatchedBuckets() - - class TechnicalsBucketsMatcher(Processor): """ Match technical indicators to buckets and saves them on the candle attachments objects. @@ -58,12 +36,10 @@ def _lazy_load_bins_file(self): if not self.bins: with open(self.bins_file_path) as bins_file_content: content = bins_file_content.read() - self.bins: BucketsContainer = DeserializationService.deserialize(json.loads(content)) + self.bins: BucketsContainer = BucketsContainer.model_validate_json(content) def process(self, context: SharedContext, candle: Candle): - normalized_indicators: NormalizedIndicators = candle.attachments.get_attachment( - NORMALIZED_INDICATORS_ATTACHMENT_KEY - ) + normalized_indicators: NormalizedIndicators = candle.get_attachment(NORMALIZED_INDICATORS_ATTACHMENT_KEY) self._lazy_load_bins_file() matched_buckets = IndicatorsMatchedBuckets() @@ -78,7 +54,7 @@ def process(self, context: SharedContext, candle: Candle): matched_buckets.set(indicator, match) - candle.attachments.add_attachement(INDICATORS_MATCHED_BUCKETS_ATTACHMENT_KEY, matched_buckets) + candle.add_attachment(INDICATORS_MATCHED_BUCKETS_ATTACHMENT_KEY, matched_buckets) super().process(context, candle) @@ -87,7 +63,7 @@ def _indicator_list_match(self, values: List[float], bins: List[BucketList]) -> def _indicator_match(self, value: float, bins: BucketList) -> Optional[Bucket]: for bin in bins: - if bin.start <= value < bin.end: + if bin.get_start <= value < bin.get_end: return bin def serialize(self) -> Dict: diff --git a/src/algotrader/pipeline/processors/technicals_normalizer.py b/src/algotrader/pipeline/processors/technicals_normalizer.py index 016e8cc..0d04c30 100644 --- a/src/algotrader/pipeline/processors/technicals_normalizer.py +++ b/src/algotrader/pipeline/processors/technicals_normalizer.py @@ -4,23 +4,18 @@ import numpy +from algotrader.entities.attachments.technicals_normalizer import NormalizedIndicators from algotrader.entities.candle import Candle -from algotrader.entities.generic_candle_attachment import GenericCandleAttachment from algotrader.pipeline.processor import Processor -from algotrader.pipeline.processors.assets_correlation import AssetCorrelation, CORRELATIONS_ATTACHMENT_KEY +from algotrader.pipeline.processors.assets_correlation import CORRELATIONS_ATTACHMENT_KEY +from algotrader.entities.attachments.assets_correlation import AssetCorrelation from algotrader.pipeline.processors.candle_cache import CandleCache -from algotrader.pipeline.processors.technicals import Indicators, INDICATORS_ATTACHMENT_KEY, IndicatorValue +from algotrader.pipeline.processors.technicals import INDICATORS_ATTACHMENT_KEY +from algotrader.entities.attachments.technicals import Indicators, IndicatorValue from algotrader.pipeline.shared_context import SharedContext NORMALIZED_INDICATORS_ATTACHMENT_KEY = "normalized_indicators" - -class NormalizedIndicators(GenericCandleAttachment[IndicatorValue]): - pass - - -NormalizedIndicators() - VWAP_NORMALIZE_PREFIXES = ["sma", "ema", "typical", "bbands"] NormalizeFunc = Callable[[List[Candle], IndicatorValue], IndicatorValue] @@ -51,8 +46,8 @@ def process(self, context: SharedContext, candle: Candle): symbol_candles = cache_reader.get_symbol_candles(candle.symbol) or [] latest_candles = symbol_candles[-self.normalization_window_size :] + [candle] - indicators: Indicators = candle.attachments.get_attachment(INDICATORS_ATTACHMENT_KEY) - asset_correlation: AssetCorrelation = candle.attachments.get_attachment(CORRELATIONS_ATTACHMENT_KEY) + indicators: Indicators = candle.get_attachment(INDICATORS_ATTACHMENT_KEY) + asset_correlation: AssetCorrelation = candle.get_attachment(CORRELATIONS_ATTACHMENT_KEY) normalized_indicators = NormalizedIndicators() for indicator_name, indicator_value in indicators.items(): @@ -64,7 +59,7 @@ def process(self, context: SharedContext, candle: Candle): if correlation: normalized_indicators.set("correlation", correlation) - candle.attachments.add_attachement(NORMALIZED_INDICATORS_ATTACHMENT_KEY, normalized_indicators) + candle.add_attachment(NORMALIZED_INDICATORS_ATTACHMENT_KEY, normalized_indicators) super().process(context, candle) diff --git a/src/algotrader/pipeline/strategies/connors_rsi2.py b/src/algotrader/pipeline/strategies/connors_rsi2.py index 7125bc0..355eeb8 100644 --- a/src/algotrader/pipeline/strategies/connors_rsi2.py +++ b/src/algotrader/pipeline/strategies/connors_rsi2.py @@ -4,7 +4,8 @@ from algotrader.entities.strategy import Strategy from algotrader.entities.strategy_signal import StrategySignal, SignalDirection from algotrader.pipeline.processors.candle_cache import CandleCache -from algotrader.pipeline.processors.technicals import INDICATORS_ATTACHMENT_KEY, Indicators +from algotrader.pipeline.processors.technicals import INDICATORS_ATTACHMENT_KEY +from algotrader.entities.attachments.technicals import Indicators from algotrader.pipeline.shared_context import SharedContext @@ -23,8 +24,8 @@ def process(self, context: SharedContext, candle: Candle) -> List[StrategySignal if candle.symbol not in self.current_position: self.current_position[candle.symbol] = None - past_candle_indicators: Indicators = symbol_candles[-1].attachments.get_attachment(INDICATORS_ATTACHMENT_KEY) - current_candle_indicators: Indicators = candle.attachments.get_attachment(INDICATORS_ATTACHMENT_KEY) + past_candle_indicators: Indicators = symbol_candles[-1].get_attachment(INDICATORS_ATTACHMENT_KEY) + current_candle_indicators: Indicators = candle.get_attachment(INDICATORS_ATTACHMENT_KEY) if ( not current_candle_indicators.has("rsi2") diff --git a/src/algotrader/pipeline/strategies/history_bucket_compare.py b/src/algotrader/pipeline/strategies/history_bucket_compare.py index 7be6d87..27cc439 100644 --- a/src/algotrader/pipeline/strategies/history_bucket_compare.py +++ b/src/algotrader/pipeline/strategies/history_bucket_compare.py @@ -2,12 +2,12 @@ from datetime import datetime from typing import List, Dict +from algotrader.entities.attachments.technicals_buckets_matcher import IndicatorsMatchedBuckets from algotrader.entities.candle import Candle from algotrader.entities.strategy import Strategy from algotrader.entities.strategy_signal import StrategySignal, SignalDirection from algotrader.pipeline.processors.technicals_buckets_matcher import ( INDICATORS_MATCHED_BUCKETS_ATTACHMENT_KEY, - IndicatorsMatchedBuckets, ) from algotrader.pipeline.shared_context import SharedContext from algotrader.serialization.store import DeserializationService @@ -34,8 +34,10 @@ def __init__( self.min_event_count = min_event_count self.min_avg_return = min_avg_return - groupby_fields = [f"attachments.indicators_matched_buckets.{ind}.ident" for ind in self.indicators_to_compare] - return_fields = [f"attachments.returns.{return_field}" for return_field in self.return_fields] + groupby_fields = [ + f"attachments.indicators_matched_buckets.data.{ind}.ident" for ind in self.indicators_to_compare + ] + return_fields = [f"attachments.returns.data.{return_field}" for return_field in self.return_fields] self.long_matchers, self.short_matchers = storage_provider.get_aggregated_history( timeframe_start, timeframe_end, groupby_fields, return_fields, min_event_count, min_avg_return @@ -44,18 +46,16 @@ def __init__( logging.info(f"Found {len(self.long_matchers)} long matchers and {len(self.short_matchers)} short matchers") def process(self, context: SharedContext, candle: Candle) -> List[StrategySignal]: - indicators_buckets: IndicatorsMatchedBuckets = candle.attachments.get_attachment( - INDICATORS_MATCHED_BUCKETS_ATTACHMENT_KEY - ) + indicators_buckets: IndicatorsMatchedBuckets = candle.get_attachment(INDICATORS_MATCHED_BUCKETS_ATTACHMENT_KEY) candle_buckets_map: Dict[str, int] = {} for indicator in self.indicators_to_compare: if not indicators_buckets.has(indicator): return [] - candle_buckets_map[f"attachments.indicators_matched_buckets.{indicator}.ident"] = indicators_buckets.get( - indicator - ).ident + candle_buckets_map[f"attachments.indicators_matched_buckets.data.{indicator}.ident"] = ( + indicators_buckets.get(indicator).ident + ) for matcher in self.long_matchers: match = True diff --git a/src/algotrader/pipeline/strategies/history_cosine_similarity.py b/src/algotrader/pipeline/strategies/history_cosine_similarity.py index 7cb95c8..a0cec08 100644 --- a/src/algotrader/pipeline/strategies/history_cosine_similarity.py +++ b/src/algotrader/pipeline/strategies/history_cosine_similarity.py @@ -7,9 +7,9 @@ from algotrader.entities.strategy import Strategy from algotrader.entities.strategy_signal import StrategySignal, SignalDirection from algotrader.pipeline.processors.technicals_buckets_matcher import ( - IndicatorsMatchedBuckets, INDICATORS_MATCHED_BUCKETS_ATTACHMENT_KEY, ) +from algotrader.entities.attachments.technicals_buckets_matcher import IndicatorsMatchedBuckets from algotrader.pipeline.shared_context import SharedContext from algotrader.serialization.store import DeserializationService from algotrader.storage.storage_provider import StorageProvider @@ -42,9 +42,7 @@ def __init__( ) def process(self, context: SharedContext, candle: Candle) -> List[StrategySignal]: - indicators_buckets: IndicatorsMatchedBuckets = candle.attachments.get_attachment( - INDICATORS_MATCHED_BUCKETS_ATTACHMENT_KEY - ) + indicators_buckets: IndicatorsMatchedBuckets = candle.get_attachment(INDICATORS_MATCHED_BUCKETS_ATTACHMENT_KEY) candle_values: list[int] = [] for indicator in self.indicators_to_compare: diff --git a/src/algotrader/pipeline/strategies/simple_sma.py b/src/algotrader/pipeline/strategies/simple_sma.py index a7ed7fe..9ee022b 100644 --- a/src/algotrader/pipeline/strategies/simple_sma.py +++ b/src/algotrader/pipeline/strategies/simple_sma.py @@ -4,7 +4,8 @@ from algotrader.entities.strategy import Strategy from algotrader.entities.strategy_signal import StrategySignal, SignalDirection from algotrader.pipeline.processors.candle_cache import CandleCache -from algotrader.pipeline.processors.technicals import INDICATORS_ATTACHMENT_KEY, Indicators +from algotrader.pipeline.processors.technicals import INDICATORS_ATTACHMENT_KEY +from algotrader.entities.attachments.technicals import Indicators from algotrader.pipeline.shared_context import SharedContext @@ -20,8 +21,8 @@ def process(self, context: SharedContext, candle: Candle) -> List[StrategySignal if not symbol_candles or len(symbol_candles) < 1: return [] - past_candle_indicators: Indicators = symbol_candles[-1].attachments.get_attachment(INDICATORS_ATTACHMENT_KEY) - current_candle_indicators: Indicators = candle.attachments.get_attachment(INDICATORS_ATTACHMENT_KEY) + past_candle_indicators: Indicators = symbol_candles[-1].get_attachment(INDICATORS_ATTACHMENT_KEY) + current_candle_indicators: Indicators = candle.get_attachment(INDICATORS_ATTACHMENT_KEY) if not current_candle_indicators.has("sma20") or not past_candle_indicators.has("sma20"): return [] diff --git a/src/algotrader/pipeline/terminators/technicals_binner.py b/src/algotrader/pipeline/terminators/technicals_binner.py index 4658033..21aab9b 100644 --- a/src/algotrader/pipeline/terminators/technicals_binner.py +++ b/src/algotrader/pipeline/terminators/technicals_binner.py @@ -1,18 +1,17 @@ from __future__ import annotations -import json import math from typing import List, Dict, Tuple +from algotrader.entities.attachments.technicals import IndicatorValue from algotrader.entities.bucket import Bucket from algotrader.entities.bucketscontainer import BucketsContainer from algotrader.entities.candle import Candle from algotrader.pipeline.processors.candle_cache import CandleCache -from algotrader.pipeline.processors.technicals import IndicatorValue from algotrader.pipeline.processors.technicals_normalizer import ( - NormalizedIndicators, NORMALIZED_INDICATORS_ATTACHMENT_KEY, ) +from algotrader.entities.attachments.technicals_normalizer import NormalizedIndicators from algotrader.pipeline.shared_context import SharedContext from algotrader.pipeline.terminator import Terminator @@ -41,9 +40,7 @@ def terminate(self, context: SharedContext): self._save_bins() def _process_candle(self, candle: Candle): - normalized_indicators: NormalizedIndicators = candle.attachments.get_attachment( - NORMALIZED_INDICATORS_ATTACHMENT_KEY - ) + normalized_indicators: NormalizedIndicators = candle.get_attachment(NORMALIZED_INDICATORS_ATTACHMENT_KEY) if not normalized_indicators: return @@ -85,7 +82,7 @@ def _get_single_float_bins(self, values: List[float]) -> List[Bucket]: def _save_bins(self): with open(self.output_file_path, "w+") as output_file: - output_file.write(json.dumps(self.bins.serialize())) + output_file.write(self.bins.model_dump_json()) def serialize(self) -> Dict: obj = super().serialize() diff --git a/src/algotrader/providers/binance.py b/src/algotrader/providers/binance.py index 2a22084..11897e2 100644 --- a/src/algotrader/providers/binance.py +++ b/src/algotrader/providers/binance.py @@ -71,7 +71,16 @@ def _deserialize_websocket_candle(self, data: Dict, interval: TimeSpan) -> Candl close = float(data["c"]) volume = float(data["v"]) - return Candle(data["s"], interval, timestamp, open, close, high, low, volume) + return Candle( + symbol=data["s"], + time_span=interval, + timestamp=timestamp, + open=open, + close=close, + high=high, + low=low, + volume=volume, + ) def _deserialize_candle(self, symbol: str, interval: TimeSpan, data: Dict) -> Candle: timestamp = self._timestamp_to_datetime(data[0]) @@ -81,7 +90,16 @@ def _deserialize_candle(self, symbol: str, interval: TimeSpan, data: Dict) -> Ca close = float(data[4]) volume = float(data[5]) - return Candle(symbol, interval, timestamp, open, close, high, low, volume) + return Candle( + symbol=symbol, + time_span=interval, + timestamp=timestamp, + open=open, + close=close, + high=high, + low=low, + volume=volume, + ) def send_bracket_order( self, diff --git a/src/algotrader/serialization/store.py b/src/algotrader/serialization/store.py index 6f3e919..75fd419 100644 --- a/src/algotrader/serialization/store.py +++ b/src/algotrader/serialization/store.py @@ -17,7 +17,7 @@ def deserialize(data: Dict) -> Optional[T]: if data is None or data.get("__class__") is None: return None - class_name = data.get("__class__") + class_name = data.get("__class__", "") mod_name, cls_name = class_name.split(":") mod = importlib.import_module(mod_name) cls: Deserializable = getattr(mod, cls_name) diff --git a/src/algotrader/storage/mongodb_storage.py b/src/algotrader/storage/mongodb_storage.py index 4ac4e30..da23576 100644 --- a/src/algotrader/storage/mongodb_storage.py +++ b/src/algotrader/storage/mongodb_storage.py @@ -8,7 +8,7 @@ from pymongo.collection import Collection from pymongo.database import Database -from algotrader.entities.candle import Candle, str_to_timestamp, timestamp_to_str +from algotrader.entities.candle import Candle from algotrader.entities.timespan import TimeSpan from algotrader.storage.storage_provider import StorageProvider @@ -51,7 +51,7 @@ def _ensure_connection(self): self.candles_collection = self.db[CANDLES_COLLECTION] self.candles_collection.create_index( - [("symbol", pymongo.ASCENDING), ("timespan", pymongo.ASCENDING), ("timestamp", pymongo.ASCENDING)], + [("symbol", pymongo.ASCENDING), ("time_span", pymongo.ASCENDING), ("timestamp", pymongo.ASCENDING)], unique=True, background=True, ) @@ -161,26 +161,12 @@ def _generate_min_fields_match_stage_short(min_count: int, return_fields: List[s def save(self, candle: Candle): self._ensure_connection() - self.candles_collection.replace_one( - self._serialize_candle_key(candle), self._serialize_candle(candle), upsert=True - ) - - def _serialize_candle_key(self, candle: Candle) -> Dict: - data = self._serialize_candle(candle) - return { - "symbol": data["symbol"], - "timespan": data["timespan"], - "timestamp": data["timestamp"], - } + self.candles_collection.replace_one(self._serialize_candle_key(candle), candle.model_dump(), upsert=True) - def _serialize_candle(self, candle: Candle) -> Dict: - data = candle.serialize() - data["timestamp"] = str_to_timestamp(data["timestamp"]) - return data - - def _deserialize_candle(self, data: Dict) -> Candle: - data["timestamp"] = timestamp_to_str(data["timestamp"]) - return Candle.deserialize(data) + def _serialize_candle_key(self, candle: Candle) -> dict: + return candle.model_dump( + include={"symbol", "time_span", "timestamp"}, exclude={"open", "high", "low", "close", "volume"} + ) def get_symbol_candles( self, symbol: str, time_span: TimeSpan, from_timestamp: datetime, to_timestamp: datetime, limit: int = 0 @@ -189,22 +175,19 @@ def get_symbol_candles( query = { "symbol": symbol, - "timespan": time_span.value, + "time_span": time_span.value, "timestamp": {"$gte": from_timestamp, "$lte": to_timestamp}, } - return [ - self._deserialize_candle(candle) - for candle in self.candles_collection.find(query).sort("timestamp").limit(limit) - ] + return [Candle(**candle) for candle in self.candles_collection.find(query).sort("timestamp").limit(limit)] def get_candles(self, time_span: TimeSpan, from_timestamp: datetime, to_timestamp: datetime) -> Iterator[Candle]: self._ensure_connection() - query = {"timespan": time_span.value, "timestamp": {"$gte": from_timestamp, "$lte": to_timestamp}} + query = {"time_span": time_span.value, "timestamp": {"$gte": from_timestamp, "$lte": to_timestamp}} for candle in self.candles_collection.find(query).sort("timestamp"): - yield self._deserialize_candle(candle) + yield Candle(**candle) def __drop_collections__(self): self._ensure_connection() diff --git a/src/algotrader/trade/simple_sum_signals_executor.py b/src/algotrader/trade/simple_sum_signals_executor.py index 833def9..d5a1836 100644 --- a/src/algotrader/trade/simple_sum_signals_executor.py +++ b/src/algotrader/trade/simple_sum_signals_executor.py @@ -23,7 +23,7 @@ def execute(self, candle: Candle, signals: List[StrategySignal]): self.position[candle.symbol] = 0 for signal in signals: - logging.info(f"Got {signal.direction} signal for {signal.symbol}. Signaling candle: {candle.serialize()}") + logging.info(f"Got {signal.direction} signal for {signal.symbol}. Signaling candle: {candle.model_dump()}") if signal.symbol not in self.position: self.position[signal.symbol] = 0 diff --git a/src/algotrader/trade/stdout_signals_executor.py b/src/algotrader/trade/stdout_signals_executor.py index 241c8b8..b838863 100644 --- a/src/algotrader/trade/stdout_signals_executor.py +++ b/src/algotrader/trade/stdout_signals_executor.py @@ -9,4 +9,4 @@ class StdoutSignalsExecutor(SignalsExecutor): def execute(self, candle: Candle, signals: List[StrategySignal]): for signal in signals: - logging.info(f"Got {signal.direction} signal for {signal.symbol}. Signaling candle: {candle.serialize()}") + logging.info(f"Got {signal.direction} signal for {signal.symbol}. Signaling candle: {candle.model_dump()}") diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py index d0fa3aa..b0fd789 100644 --- a/tests/unit/__init__.py +++ b/tests/unit/__init__.py @@ -1,5 +1,6 @@ from datetime import datetime + from algotrader.entities.candle import Candle from algotrader.entities.timespan import TimeSpan diff --git a/tests/unit/strategies/test_history_compare.py b/tests/unit/strategies/test_history_compare.py index 3440ab6..7855d60 100644 --- a/tests/unit/strategies/test_history_compare.py +++ b/tests/unit/strategies/test_history_compare.py @@ -11,12 +11,13 @@ from algotrader.entities.timespan import TimeSpan from fakes.strategy_executor import FakeSignalsExecutor from algotrader.pipeline.processors.candle_cache import CandleCache -from algotrader.pipeline.processors.returns import Returns, RETURNS_ATTACHMENT_KEY +from algotrader.pipeline.processors.returns import RETURNS_ATTACHMENT_KEY +from algotrader.entities.attachments.returns import Returns from algotrader.pipeline.processors.strategy import StrategyProcessor from algotrader.pipeline.processors.technicals_buckets_matcher import ( - IndicatorsMatchedBuckets, INDICATORS_MATCHED_BUCKETS_ATTACHMENT_KEY, ) +from algotrader.entities.attachments.technicals_buckets_matcher import IndicatorsMatchedBuckets from algotrader.pipeline.shared_context import SharedContext from algotrader.pipeline.strategies.history_bucket_compare import HistoryBucketCompareStrategy from algotrader.storage.mongodb_storage import MongoDBStorage @@ -44,8 +45,8 @@ def _check(signals: List[StrategySignal]): history_compare_strategy = HistoryBucketCompareStrategy( mongodb_storage, - datetime.now() - timedelta(days=60), - datetime.now(), + datetime.utcnow() - timedelta(days=60), + datetime.utcnow(), indicators_to_compare=["sma5", "sma20"], return_fields=["ctc1"], min_event_count=1, @@ -119,7 +120,7 @@ def _check(signals: List[StrategySignal]): processor.process(context, candle) def _get_history_candle(self, ctc_value: float) -> Candle: - candle = generate_candle(TimeSpan.Day, datetime.now() - timedelta(days=random.randint(2, 50))) + candle = generate_candle(TimeSpan.Day, datetime.utcnow() - timedelta(days=random.randint(2, 50))) indicator_buckets = IndicatorsMatchedBuckets() indicator_buckets.set("sma5", Bucket(ident=0, start=0, end=0)) @@ -128,8 +129,8 @@ def _get_history_candle(self, ctc_value: float) -> Candle: candle_returns = Returns() candle_returns.set("ctc1", ctc_value) - candle.attachments.add_attachement(INDICATORS_MATCHED_BUCKETS_ATTACHMENT_KEY, indicator_buckets) - candle.attachments.add_attachement(RETURNS_ATTACHMENT_KEY, candle_returns) + candle.add_attachment(INDICATORS_MATCHED_BUCKETS_ATTACHMENT_KEY, indicator_buckets) + candle.add_attachment(RETURNS_ATTACHMENT_KEY, candle_returns) return candle @@ -140,5 +141,5 @@ def _get_candle(self) -> Candle: indicator_buckets.set("sma5", Bucket(ident=0, start=0, end=0)) indicator_buckets.set("sma20", Bucket(ident=4, start=7, end=9)) - candle.attachments.add_attachement(INDICATORS_MATCHED_BUCKETS_ATTACHMENT_KEY, indicator_buckets) + candle.add_attachment(INDICATORS_MATCHED_BUCKETS_ATTACHMENT_KEY, indicator_buckets) return candle diff --git a/tests/unit/strategies/test_simple_sma.py b/tests/unit/strategies/test_simple_sma.py index 52be8fa..367ff2f 100644 --- a/tests/unit/strategies/test_simple_sma.py +++ b/tests/unit/strategies/test_simple_sma.py @@ -8,7 +8,8 @@ from fakes.strategy_executor import FakeSignalsExecutor from algotrader.pipeline.processors.candle_cache import CandleCache from algotrader.pipeline.processors.strategy import StrategyProcessor -from algotrader.pipeline.processors.technicals import INDICATORS_ATTACHMENT_KEY, Indicators +from algotrader.pipeline.processors.technicals import INDICATORS_ATTACHMENT_KEY +from algotrader.entities.attachments.technicals import Indicators from algotrader.pipeline.shared_context import SharedContext from algotrader.pipeline.strategies.simple_sma import SimpleSMA from unit import generate_candle, TEST_SYMBOL @@ -57,6 +58,6 @@ def _get_candles(self) -> Tuple[Candle, Candle]: current_indicators.set("sma5", 6) current_indicators.set("sma20", 5) - prev_candle.attachments.add_attachement(INDICATORS_ATTACHMENT_KEY, prev_indicators) - current_candle.attachments.add_attachement(INDICATORS_ATTACHMENT_KEY, current_indicators) + prev_candle.add_attachment(INDICATORS_ATTACHMENT_KEY, prev_indicators) + current_candle.add_attachment(INDICATORS_ATTACHMENT_KEY, current_indicators) return prev_candle, current_candle diff --git a/tests/unit/test_asset_correlation.py b/tests/unit/test_asset_correlation.py index 46aa30e..3cd8bdd 100644 --- a/tests/unit/test_asset_correlation.py +++ b/tests/unit/test_asset_correlation.py @@ -11,10 +11,10 @@ from algotrader.pipeline.configs.technical_processor_config import TechnicalsProcessorConfig from algotrader.pipeline.pipeline import Pipeline from algotrader.pipeline.processors.assets_correlation import ( - AssetCorrelation, CORRELATIONS_ATTACHMENT_KEY, AssetCorrelationProcessor, ) +from algotrader.entities.attachments.assets_correlation import AssetCorrelation from algotrader.pipeline.processors.candle_cache import CandleCache from algotrader.pipeline.processors.technicals import TechnicalsProcessor from algotrader.pipeline.processors.timespan_change import TimeSpanChangeProcessor @@ -64,9 +64,7 @@ def _check(context: SharedContext, candle: Candle): if check_count > 20: cache_reader = CandleCache.context_reader(context) latest_candle = cache_reader.get_symbol_candles(candle.symbol)[-2] - asset_correlation: AssetCorrelation = latest_candle.attachments.get_attachment( - CORRELATIONS_ATTACHMENT_KEY - ) + asset_correlation: AssetCorrelation = latest_candle.get_attachment(CORRELATIONS_ATTACHMENT_KEY) if candle.symbol == "X": self.assertFalse(asset_correlation.has("X")) self.assertTrue(asset_correlation.has("Y")) diff --git a/tests/unit/test_filesink_processor.py b/tests/unit/test_filesink_processor.py index df3af5a..641c029 100644 --- a/tests/unit/test_filesink_processor.py +++ b/tests/unit/test_filesink_processor.py @@ -31,7 +31,7 @@ def _check(context: SharedContext): lines = temp_file.readlines() self.assertEqual(49, len(lines)) for line in lines: - candle = Candle.deserialize(json.loads(line)) + candle = Candle.model_validate_json(json.loads(line)) self.assertEqual(TimeSpan.Day, candle.time_span) self.assertEqual(datetime.now().day, candle.timestamp.day) diff --git a/tests/unit/test_returns_calculator_processor.py b/tests/unit/test_returns_calculator_processor.py index 1bf8050..6149dd7 100644 --- a/tests/unit/test_returns_calculator_processor.py +++ b/tests/unit/test_returns_calculator_processor.py @@ -26,13 +26,13 @@ def _check_returns(context: SharedContext): cache_reader = CandleCache.context_reader(context) candles = cache_reader.get_symbol_candles(TEST_SYMBOL) - self.assertFalse(candles[0].attachments.get_attachment(RETURNS_ATTACHMENT_KEY).has("ctc-1")) - self.assertFalse(candles[1].attachments.get_attachment(RETURNS_ATTACHMENT_KEY).has("ctc-1")) - self.assertFalse(candles[2].attachments.get_attachment(RETURNS_ATTACHMENT_KEY).has("ctc-1")) + self.assertFalse(candles[0].get_attachment(RETURNS_ATTACHMENT_KEY).has("ctc-1")) + self.assertFalse(candles[1].get_attachment(RETURNS_ATTACHMENT_KEY).has("ctc-1")) + self.assertFalse(candles[2].get_attachment(RETURNS_ATTACHMENT_KEY).has("ctc-1")) - ctc1 = candles[3].attachments.get_attachment(RETURNS_ATTACHMENT_KEY)["ctc-1"] - ctc2 = candles[3].attachments.get_attachment(RETURNS_ATTACHMENT_KEY)["ctc-2"] - ctc3 = candles[3].attachments.get_attachment(RETURNS_ATTACHMENT_KEY)["ctc-3"] + ctc1 = candles[3].get_attachment(RETURNS_ATTACHMENT_KEY)["ctc-1"] + ctc2 = candles[3].get_attachment(RETURNS_ATTACHMENT_KEY)["ctc-2"] + ctc3 = candles[3].get_attachment(RETURNS_ATTACHMENT_KEY)["ctc-3"] self.assertTrue(ctc1 < ctc2 < ctc3) cache_processor = CandleCache() diff --git a/tests/unit/test_serializations.py b/tests/unit/test_serializations.py index f400d5d..6c0dedb 100644 --- a/tests/unit/test_serializations.py +++ b/tests/unit/test_serializations.py @@ -1,29 +1,20 @@ from datetime import datetime -from typing import Dict from unittest import TestCase +from algotrader.entities.attachments.nothing import NothingClass from algotrader.entities.bucket import Bucket from algotrader.entities.bucketscontainer import BucketsContainer from algotrader.entities.candle import Candle -from algotrader.entities.serializable import Serializable, Deserializable from algotrader.entities.timespan import TimeSpan -from algotrader.serialization.store import DeserializationService from unit import generate_candle_with_price -class NothingClass(Serializable, Deserializable): - def serialize(self) -> Dict: - obj = super().serialize() - obj.update({"nothing": "at-all"}) - return obj - - class TestSerializations(TestCase): def test_candle(self): candle = generate_candle_with_price(TimeSpan.Day, datetime.now(), 888) - data = candle.serialize() + data = candle.model_dump_json() - new_candle = Candle.deserialize(data) + new_candle = Candle.model_validate_json(data) self.assertEqual(candle.symbol, new_candle.symbol) self.assertEqual(candle.timestamp, new_candle.timestamp) @@ -36,14 +27,14 @@ def test_candle(self): def test_candle_attachments(self): candle = generate_candle_with_price(TimeSpan.Day, datetime.now(), 888) - candle.add_attachement("key1", NothingClass()) + candle.add_attachment("key1", NothingClass()) - data = candle.serialize() - new_candle = Candle.deserialize(data) + data = candle.model_dump_json() + new_candle = Candle.model_validate_json(data) self.assertEqual(candle.symbol, new_candle.symbol) - original_attachment = candle.attachments.get_attachment("key1") - new_attachment = new_candle.attachments.get_attachment("key1") + original_attachment = candle.get_attachment("key1") + new_attachment = new_candle.get_attachment("key1") self.assertEqual(original_attachment.__class__, new_attachment.__class__) def test_bins(self): @@ -51,8 +42,8 @@ def test_bins(self): bins.add("x", [Bucket(ident=1, start=1, end=2)]) bins.add("list", [[Bucket(ident=0, start=1, end=2)], [Bucket(ident=1, start=3, end=4)]]) - serialized_data = bins.serialize() - new_bins: BucketsContainer = DeserializationService.deserialize(serialized_data) + serialized_data = bins.model_dump_json() + new_bins = BucketsContainer.model_validate_json(serialized_data) x = new_bins.get("x") self.assertIsNotNone(x) diff --git a/tests/unit/test_technicals_binner_terminator.py b/tests/unit/test_technicals_binner_terminator.py index 3c91119..74ddafa 100644 --- a/tests/unit/test_technicals_binner_terminator.py +++ b/tests/unit/test_technicals_binner_terminator.py @@ -15,14 +15,14 @@ from algotrader.pipeline.processors.technicals import TechnicalsProcessor from algotrader.pipeline.processors.technicals_buckets_matcher import ( TechnicalsBucketsMatcher, - IndicatorsMatchedBuckets, INDICATORS_MATCHED_BUCKETS_ATTACHMENT_KEY, ) +from algotrader.entities.attachments.technicals_buckets_matcher import IndicatorsMatchedBuckets from algotrader.pipeline.processors.technicals_normalizer import ( TechnicalsNormalizerProcessor, - NormalizedIndicators, NORMALIZED_INDICATORS_ATTACHMENT_KEY, ) +from algotrader.entities.attachments.technicals_normalizer import NormalizedIndicators from algotrader.pipeline.runner import PipelineRunner from algotrader.pipeline.shared_context import SharedContext from algotrader.pipeline.terminators.technicals_binner import TechnicalsBinner @@ -61,19 +61,19 @@ def _check(context: SharedContext, candle: Candle): check_count = context.get_kv_data("check_count", 0) if check_count > 20: - matched_buckets: IndicatorsMatchedBuckets = candle.attachments.get_attachment( + matched_buckets: IndicatorsMatchedBuckets = candle.get_attachment( INDICATORS_MATCHED_BUCKETS_ATTACHMENT_KEY ) - normalized_indicators: NormalizedIndicators = candle.attachments.get_attachment( + normalized_indicators: NormalizedIndicators = candle.get_attachment( NORMALIZED_INDICATORS_ATTACHMENT_KEY ) matched_bucket = matched_buckets.get("sma5") indicator_value = normalized_indicators.get("sma5") self.assertTrue(isinstance(matched_bucket, Bucket)) - self.assertTrue(matched_bucket.start <= indicator_value) - self.assertTrue(matched_bucket.end > indicator_value) + self.assertTrue(matched_bucket.get_start <= indicator_value) + self.assertTrue(matched_bucket.get_end > indicator_value) with tempfile.TemporaryDirectory() as tempdir: tmpfilepath = os.path.join(tempdir, "temp_bin_file.dat") diff --git a/tests/unit/test_technicals_processor.py b/tests/unit/test_technicals_processor.py index ea9e5fc..95bc878 100644 --- a/tests/unit/test_technicals_processor.py +++ b/tests/unit/test_technicals_processor.py @@ -8,12 +8,13 @@ from algotrader.pipeline.configs.technical_processor_config import TechnicalsProcessorConfig from algotrader.pipeline.pipeline import Pipeline from algotrader.pipeline.processors.candle_cache import CandleCache -from algotrader.pipeline.processors.technicals import TechnicalsProcessor, INDICATORS_ATTACHMENT_KEY, Indicators +from algotrader.pipeline.processors.technicals import TechnicalsProcessor, INDICATORS_ATTACHMENT_KEY +from algotrader.entities.attachments.technicals import Indicators from algotrader.pipeline.processors.technicals_normalizer import ( TechnicalsNormalizerProcessor, NORMALIZED_INDICATORS_ATTACHMENT_KEY, - NormalizedIndicators, ) +from algotrader.entities.attachments.technicals_normalizer import NormalizedIndicators from algotrader.pipeline.runner import PipelineRunner from algotrader.pipeline.shared_context import SharedContext from fakes.pipeline_validators import ValidationProcessor @@ -33,7 +34,7 @@ def _check(context: SharedContext, candle: Candle): check_count = context.get_kv_data("check_count", 0) if check_count > 20: - candle_indicators: Indicators = candle.attachments.get_attachment(INDICATORS_ATTACHMENT_KEY) + candle_indicators: Indicators = candle.attachments[INDICATORS_ATTACHMENT_KEY] macd_values = candle_indicators.get("macd") self.assertEqual(len(macd_values), 3) self.assertIsNotNone(macd_values[0]) @@ -68,11 +69,9 @@ def _check(context: SharedContext, candle: Candle): check_count = context.get_kv_data("check_count", 0) if check_count > 20: - indicators: Indicators = candle.attachments.get_attachment(INDICATORS_ATTACHMENT_KEY) + indicators: Indicators = candle.attachments[INDICATORS_ATTACHMENT_KEY] - normalized_indicators: NormalizedIndicators = candle.attachments.get_attachment( - NORMALIZED_INDICATORS_ATTACHMENT_KEY - ) + normalized_indicators: NormalizedIndicators = candle.attachments[NORMALIZED_INDICATORS_ATTACHMENT_KEY] bbands5_value = indicators.get("bbands5") normalized_bbands5_value = normalized_indicators.get("bbands5")