Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f3a0b9149 | |||
| 49c0fd7dd5 | |||
| 3d98592461 | |||
| 124efb4432 | |||
| 4d0bc08efd | |||
| c2c85c7ecf | |||
| 04ae145126 | |||
| e4ca9c3f8f | |||
| d43bc9ddf3 | |||
| 244d754707 | |||
| 03ceac1f3b | |||
| 73415cd2dc | |||
| ad51dbc1a3 | |||
| f09057aaf1 | |||
| 458ef2385e | |||
| 2d140419bb | |||
| 8e5bfd07ce | |||
| 5eb820a9c7 | |||
| fae60e0d54 | |||
| 433b06367f | |||
| 3dd7010129 | |||
| 4f11df0e33 | |||
| b5d9e47a2e | |||
| 511d3a27f7 | |||
| 5b23b36261 | |||
| 5867f71450 | |||
| 2be21df803 | |||
| 498b74a5ae | |||
| 921a250715 | |||
| 0479191b66 |
@@ -0,0 +1,3 @@
|
||||
# Shell scripts must keep LF line endings so they run on Linux/macOS CI and
|
||||
# local shells regardless of the committer's platform autocrlf setting.
|
||||
*.sh text eol=lf
|
||||
@@ -6,6 +6,8 @@ updates:
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 10
|
||||
cooldown:
|
||||
default-days: 7
|
||||
commit-message:
|
||||
prefix: "deps(cargo)"
|
||||
|
||||
@@ -15,6 +17,8 @@ updates:
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 10
|
||||
cooldown:
|
||||
default-days: 7
|
||||
commit-message:
|
||||
prefix: "deps(npm)"
|
||||
|
||||
@@ -24,9 +28,26 @@ updates:
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 10
|
||||
cooldown:
|
||||
default-days: 7
|
||||
commit-message:
|
||||
prefix: "deps(pip)"
|
||||
|
||||
# Hash-pinned CI/bench Python tooling under .github/requirements/. Each
|
||||
# <name>.in is the loose source; the matching hash-locked <name>.txt is the
|
||||
# output regenerated by scripts/update-lockfiles.sh (uv). Dependabot keeps the
|
||||
# pins fresh; ci-dev-py39.in caps numpy <2.1 so 3.9 stays installable. Any
|
||||
# bump that breaks a matrix row surfaces in the PR's CI run.
|
||||
- package-ecosystem: pip
|
||||
directory: "/.github/requirements"
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 10
|
||||
cooldown:
|
||||
default-days: 7
|
||||
commit-message:
|
||||
prefix: "deps(ci-pip)"
|
||||
|
||||
# GitHub Actions — keeps the SHA-pinned actions current (Dependabot reads
|
||||
# the version comment after each pinned SHA and bumps both together).
|
||||
- package-ecosystem: github-actions
|
||||
@@ -34,5 +55,7 @@ updates:
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 10
|
||||
cooldown:
|
||||
default-days: 7
|
||||
commit-message:
|
||||
prefix: "deps(actions)"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Python deps + peer TA libraries for the bench.yml cross-library benchmark.
|
||||
# Loose source spec — the pinned, hash-locked output is generated from this:
|
||||
# bench.txt (Python 3.11) via scripts/update-lockfiles.sh
|
||||
# bench.yml runs on a single Python version (3.11), so one output suffices.
|
||||
maturin
|
||||
numpy
|
||||
pandas
|
||||
talipp
|
||||
finta
|
||||
@@ -0,0 +1,167 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# ./scripts/update-lockfiles.sh
|
||||
finta==1.3 \
|
||||
--hash=sha256:b94b94df311c18bf5402eb2fe8fd2db5e1bdaff08baf58a7367d05c7abdd10d3 \
|
||||
--hash=sha256:f2fa0673748f4be8f57e57cf6d5c00a4d44bc6071ea69dbb9a1d329d045cbba2
|
||||
# via -r .github/requirements/bench.in
|
||||
maturin==1.13.3 \
|
||||
--hash=sha256:0ef257e692cc756c87af5bea95ddfe7d3ac49d3376a7a87f728d63f06e7b6f8b \
|
||||
--hash=sha256:1cc0a110b224ca90406b668a3e3c1f5a515062e59e26292f6dbaf5fd4909c6f3 \
|
||||
--hash=sha256:2389fe92d017cea9d94e521fa0175314a4c52f79a1057b901fbc9f8686ef7d0b \
|
||||
--hash=sha256:3cc13929ca82aefa4adbf0f2c35419369796213c6fb0eb24e914945f50ef5d8c \
|
||||
--hash=sha256:3db93337ed97e60ffc878aa8b493cd7ae44d3a5e1a37256db3a4491f57565018 \
|
||||
--hash=sha256:4667ef609ab446c1b5e0bfe4f9fb99699ab6d8548433f8d1a684256e0b67217f \
|
||||
--hash=sha256:49fd6ab08da28098ccf37afca24cdba72376ba9c1eedf9dd25ff82ed771961ff \
|
||||
--hash=sha256:4cd478e6e4c56251e48ed079b8efd55b30bc5c09cf695a1bdafaeb582ee735a0 \
|
||||
--hash=sha256:53b08bd075649ce96513ad9abf241a43cb685ed6e9e7790f8dbc2d66e95d8323 \
|
||||
--hash=sha256:771e1e9e71a278e56db01552e0d1acfd1464259f9575b6e72842f893cd299079 \
|
||||
--hash=sha256:a2675e25f313034ae6f57388cf14818f87d8961c4a96795287f3e155f59beb11 \
|
||||
--hash=sha256:b6741d7bf4af97da937528fd1e523c6ab54f53d9a21870fa735d6e67fd88e273 \
|
||||
--hash=sha256:c00ea6428dea17bf616fe93770837634454b28c2de1a876e42ef8036c616079a \
|
||||
--hash=sha256:def4a435ea9d2ee93b18ba579dc8c9cf898889a66f312cd379b5e374ec3e3ad6
|
||||
# via -r .github/requirements/bench.in
|
||||
numpy==2.4.6 \
|
||||
--hash=sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1 \
|
||||
--hash=sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4 \
|
||||
--hash=sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f \
|
||||
--hash=sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079 \
|
||||
--hash=sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096 \
|
||||
--hash=sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47 \
|
||||
--hash=sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66 \
|
||||
--hash=sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d \
|
||||
--hash=sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1 \
|
||||
--hash=sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e \
|
||||
--hash=sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147 \
|
||||
--hash=sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd \
|
||||
--hash=sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75 \
|
||||
--hash=sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063 \
|
||||
--hash=sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73 \
|
||||
--hash=sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab \
|
||||
--hash=sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4 \
|
||||
--hash=sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41 \
|
||||
--hash=sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402 \
|
||||
--hash=sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698 \
|
||||
--hash=sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7 \
|
||||
--hash=sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8 \
|
||||
--hash=sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b \
|
||||
--hash=sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8 \
|
||||
--hash=sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0 \
|
||||
--hash=sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662 \
|
||||
--hash=sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91 \
|
||||
--hash=sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0 \
|
||||
--hash=sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f \
|
||||
--hash=sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3 \
|
||||
--hash=sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f \
|
||||
--hash=sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67 \
|
||||
--hash=sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6 \
|
||||
--hash=sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997 \
|
||||
--hash=sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b \
|
||||
--hash=sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e \
|
||||
--hash=sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538 \
|
||||
--hash=sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627 \
|
||||
--hash=sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93 \
|
||||
--hash=sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02 \
|
||||
--hash=sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853 \
|
||||
--hash=sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c \
|
||||
--hash=sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43 \
|
||||
--hash=sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd \
|
||||
--hash=sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8 \
|
||||
--hash=sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089 \
|
||||
--hash=sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778 \
|
||||
--hash=sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1 \
|
||||
--hash=sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb \
|
||||
--hash=sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261 \
|
||||
--hash=sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb \
|
||||
--hash=sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a \
|
||||
--hash=sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8 \
|
||||
--hash=sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359 \
|
||||
--hash=sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5 \
|
||||
--hash=sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7 \
|
||||
--hash=sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751 \
|
||||
--hash=sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8 \
|
||||
--hash=sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605 \
|
||||
--hash=sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e \
|
||||
--hash=sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45 \
|
||||
--hash=sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2 \
|
||||
--hash=sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895 \
|
||||
--hash=sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe \
|
||||
--hash=sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb \
|
||||
--hash=sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a \
|
||||
--hash=sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577 \
|
||||
--hash=sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d \
|
||||
--hash=sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a \
|
||||
--hash=sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda \
|
||||
--hash=sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6 \
|
||||
--hash=sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20
|
||||
# via
|
||||
# -r .github/requirements/bench.in
|
||||
# finta
|
||||
# pandas
|
||||
pandas==3.0.3 \
|
||||
--hash=sha256:0383c72c75cdcca61a9e116e611143902dbfd08bff356829c2f6d1cf40a9ca8c \
|
||||
--hash=sha256:05f1f1752b8533ea03f7f39a9c15b1a058d067bb48f4748948e7a8691e0510f2 \
|
||||
--hash=sha256:08d789b41f87e0905880e293cedf6197ce71fe67cc081358b1e148a491b9bd13 \
|
||||
--hash=sha256:0d589105b3c14645af1738ff279b2995102d8f7a03b0a66dc8d95550eb513e04 \
|
||||
--hash=sha256:13fc1e853d9e04743d11ba75a985ccbc2a317fe07d8af61e445a6fd24dacd6a6 \
|
||||
--hash=sha256:14da8316da4d0c5a77618425996bfb1248ca87fc2c1486e6fde4652bd18b5824 \
|
||||
--hash=sha256:1928e07221f82db493cd4af1e23c1bfca524a19a4699887975bff68f49a72bfb \
|
||||
--hash=sha256:261e308dfb22448384b7580cf719d2f998fe2966c92893c3e77d14008af1f066 \
|
||||
--hash=sha256:275c14e0fce14a2ec20eee474aecd305478ea3c1e6f6a9d8fe219a165542717e \
|
||||
--hash=sha256:335f62418ed562cfc3c49e9e196375c28b729dcef8543abf4f9438e381bf3c76 \
|
||||
--hash=sha256:3650109c0f22879df8bd6179ab9ee3d7f1d1d4e7e0094a3f0032d9f51e2e64ac \
|
||||
--hash=sha256:39436b377d56d2a2e52d0395bdbee171f01068e99af5250509aceeb929f765c7 \
|
||||
--hash=sha256:3c20a521bbb85902f79f7270c80a59e1b5452d96d170c034f207181870f97ac5 \
|
||||
--hash=sha256:3e91cec1879ada0624fc3dc9953c5cbd60208e59c0db28f540c5d6d47502422f \
|
||||
--hash=sha256:455f6f8139d4282188f526868dbc3c828470e88a3d9d59a891bd46a455f21b98 \
|
||||
--hash=sha256:46997386d528eb40376ecd6b033cf4a8a1e5282580f68f43de875b78cba2199d \
|
||||
--hash=sha256:4db8c527972a821cf5286b40ccc57642a39bc62e62022b42f99f8a67fca8c3a1 \
|
||||
--hash=sha256:4e15135e2ee5df1063313e2425ceef8ac0f4ae775893815b0923651b806a5639 \
|
||||
--hash=sha256:51b1fe551acb77dac643c6fda86084d8d446c10fe64b06a9cc29c4cc8540e7f2 \
|
||||
--hash=sha256:557409bc4178e70ee8d9ddb494798e51ebf6ea59330f6be22c51bab2a7db6c49 \
|
||||
--hash=sha256:5cc09a68b3120e0f54870dede8287a7bb1fa463907e4fcec1ea77cab6179bf7a \
|
||||
--hash=sha256:60ae316d3fd75d1858d450d0db0103ea2be3e7d4a95ec2f064f7e2ae63f7b028 \
|
||||
--hash=sha256:6674ab18ad8c57802867264b00e15e7bb904700cdd9046e3b2fa1fce237439ea \
|
||||
--hash=sha256:67b3b64c11910cfa29f4e94a14d3bff9ee693b6fc76055e7cad549cee0aec5fa \
|
||||
--hash=sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc \
|
||||
--hash=sha256:6dc0b3fd2169c9157deed50b4d519553a3655c8c6a96027136d654592be973a9 \
|
||||
--hash=sha256:7e65d5407dc0b394f509699650e4a2ec01c0514f21850f453fa60f3be79a5dbf \
|
||||
--hash=sha256:819959dab7bbd0049c15623fbac4e29a191b9528160a61fb1032242d8ced2d9c \
|
||||
--hash=sha256:8a1e45c80cceb3b4a21bc5939d52e8cbd8d9b7305309219d59e9754d9ce09e27 \
|
||||
--hash=sha256:9c39be2d709d01fa972a0cabc522389fceca4f3969332ba25a7d6c5802cf976a \
|
||||
--hash=sha256:9d71c63ae4ebdbf70209742096f1fc46a83a0613c99d4b23766cced9ff8cd62a \
|
||||
--hash=sha256:a2d2dff8a04f3917b55ab3910c32990f8ddf7eceba114947838cefa976a68977 \
|
||||
--hash=sha256:a4eeb6830daf35a71cc09649bd823e2b542dac246cdee9614c6e4bd65028cd6a \
|
||||
--hash=sha256:a55066a0505dae0ba2b50a46637db34b46f9094c65c5d4800794ef6335010938 \
|
||||
--hash=sha256:a82d532a3351d435432cd913edbccaf8b8e01d4dd0e5ced5a8d2e8ecd94c7e44 \
|
||||
--hash=sha256:b168fc218fd80a6cbdbdbc1a97ddc7889ed057d7eb45f50d866ceab5f39904c4 \
|
||||
--hash=sha256:b2c95f8bfc1ee412bf482605d7bfd30c12d1d26bd59fdd91efeef1d4718decb1 \
|
||||
--hash=sha256:ba7e08b9ac1d54569cd1e256e3668975ed624d6826f7b68df0342b012007bddb \
|
||||
--hash=sha256:bab900348131a7db1f69a7309ef141fd5680f1487094193bcbbb61791573bf8f \
|
||||
--hash=sha256:bd3a518890b400d32f9023722dc9a9a5c969f00b415419a3c06c043f09bb5d7d \
|
||||
--hash=sha256:c7be265b62cef88e253a941e4698604973736dcfe242fdb5198f0f7bc473cdcc \
|
||||
--hash=sha256:d26cbe1fcfc12e8fd900e2454163e466b2d3af84f7c75481df7683ffc073d870 \
|
||||
--hash=sha256:d4be06d68f9ddcfc645b87534911da79a8fbffc7573c80e0edcf42a5020624d8 \
|
||||
--hash=sha256:d72828c20c6d6e83e1e22a6a3b47b326b71664112fa9705dcbccfd7a39b62085 \
|
||||
--hash=sha256:dd1a5d1def6a46002e964510bdc67c368aa0951df5d1d9f8365336f5a1f490cd \
|
||||
--hash=sha256:e3a2ec42c98ffa2565a67e08e218d06d72576d758d90facb7c00805194d8f360 \
|
||||
--hash=sha256:f8894dc474d648fe7b6ff0ca9b0bd73950d19952bc1a6534540762c5d79d305c \
|
||||
--hash=sha256:fed2ff7fd9779120e388e285fc029bd5cf9490cdd2e4166a9ee22c0e49a9ab09
|
||||
# via
|
||||
# -r .github/requirements/bench.in
|
||||
# finta
|
||||
python-dateutil==2.9.0.post0 \
|
||||
--hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \
|
||||
--hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427
|
||||
# via pandas
|
||||
six==1.17.0 \
|
||||
--hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \
|
||||
--hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81
|
||||
# via python-dateutil
|
||||
talipp==2.7.0 \
|
||||
--hash=sha256:567f59ad74366cb59a14a00d350f35fd9d22e6924d6228bad581e6dcf1de2205 \
|
||||
--hash=sha256:f749f22b9ad615605e71faf26457bb7f5e3fe16f04d3287f4ca54fd16bc3d4eb
|
||||
# via -r .github/requirements/bench.in
|
||||
tzdata==2026.2 \
|
||||
--hash=sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10 \
|
||||
--hash=sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7
|
||||
# via pandas
|
||||
@@ -0,0 +1,7 @@
|
||||
# Python 3.10+ dev/test tooling for the ci.yml binding test job
|
||||
# (covers the 3.11 / 3.12 / 3.13 matrix rows). Locked output: ci-dev-py3.txt
|
||||
# Refresh via scripts/update-lockfiles.sh.
|
||||
maturin
|
||||
pytest
|
||||
numpy
|
||||
hypothesis
|
||||
@@ -0,0 +1,124 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# ./scripts/update-lockfiles.sh
|
||||
colorama==0.4.6 \
|
||||
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
|
||||
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
|
||||
# via pytest
|
||||
hypothesis==6.155.1 \
|
||||
--hash=sha256:07c102031612b98d7c1be15ca3608c43e1234d9d07e3a190a53fa01536700196 \
|
||||
--hash=sha256:2753f469df3ba3c483b08e0c37dbcbc41d8316ebb921abcc07493ee9c8a7d187
|
||||
# via -r .github/requirements/ci-dev-py3.in
|
||||
iniconfig==2.3.0 \
|
||||
--hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \
|
||||
--hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12
|
||||
# via pytest
|
||||
maturin==1.13.3 \
|
||||
--hash=sha256:0ef257e692cc756c87af5bea95ddfe7d3ac49d3376a7a87f728d63f06e7b6f8b \
|
||||
--hash=sha256:1cc0a110b224ca90406b668a3e3c1f5a515062e59e26292f6dbaf5fd4909c6f3 \
|
||||
--hash=sha256:2389fe92d017cea9d94e521fa0175314a4c52f79a1057b901fbc9f8686ef7d0b \
|
||||
--hash=sha256:3cc13929ca82aefa4adbf0f2c35419369796213c6fb0eb24e914945f50ef5d8c \
|
||||
--hash=sha256:3db93337ed97e60ffc878aa8b493cd7ae44d3a5e1a37256db3a4491f57565018 \
|
||||
--hash=sha256:4667ef609ab446c1b5e0bfe4f9fb99699ab6d8548433f8d1a684256e0b67217f \
|
||||
--hash=sha256:49fd6ab08da28098ccf37afca24cdba72376ba9c1eedf9dd25ff82ed771961ff \
|
||||
--hash=sha256:4cd478e6e4c56251e48ed079b8efd55b30bc5c09cf695a1bdafaeb582ee735a0 \
|
||||
--hash=sha256:53b08bd075649ce96513ad9abf241a43cb685ed6e9e7790f8dbc2d66e95d8323 \
|
||||
--hash=sha256:771e1e9e71a278e56db01552e0d1acfd1464259f9575b6e72842f893cd299079 \
|
||||
--hash=sha256:a2675e25f313034ae6f57388cf14818f87d8961c4a96795287f3e155f59beb11 \
|
||||
--hash=sha256:b6741d7bf4af97da937528fd1e523c6ab54f53d9a21870fa735d6e67fd88e273 \
|
||||
--hash=sha256:c00ea6428dea17bf616fe93770837634454b28c2de1a876e42ef8036c616079a \
|
||||
--hash=sha256:def4a435ea9d2ee93b18ba579dc8c9cf898889a66f312cd379b5e374ec3e3ad6
|
||||
# via -r .github/requirements/ci-dev-py3.in
|
||||
numpy==2.4.6 \
|
||||
--hash=sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1 \
|
||||
--hash=sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4 \
|
||||
--hash=sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f \
|
||||
--hash=sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079 \
|
||||
--hash=sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096 \
|
||||
--hash=sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47 \
|
||||
--hash=sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66 \
|
||||
--hash=sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d \
|
||||
--hash=sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1 \
|
||||
--hash=sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e \
|
||||
--hash=sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147 \
|
||||
--hash=sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd \
|
||||
--hash=sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75 \
|
||||
--hash=sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063 \
|
||||
--hash=sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73 \
|
||||
--hash=sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab \
|
||||
--hash=sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4 \
|
||||
--hash=sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41 \
|
||||
--hash=sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402 \
|
||||
--hash=sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698 \
|
||||
--hash=sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7 \
|
||||
--hash=sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8 \
|
||||
--hash=sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b \
|
||||
--hash=sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8 \
|
||||
--hash=sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0 \
|
||||
--hash=sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662 \
|
||||
--hash=sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91 \
|
||||
--hash=sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0 \
|
||||
--hash=sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f \
|
||||
--hash=sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3 \
|
||||
--hash=sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f \
|
||||
--hash=sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67 \
|
||||
--hash=sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6 \
|
||||
--hash=sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997 \
|
||||
--hash=sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b \
|
||||
--hash=sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e \
|
||||
--hash=sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538 \
|
||||
--hash=sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627 \
|
||||
--hash=sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93 \
|
||||
--hash=sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02 \
|
||||
--hash=sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853 \
|
||||
--hash=sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c \
|
||||
--hash=sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43 \
|
||||
--hash=sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd \
|
||||
--hash=sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8 \
|
||||
--hash=sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089 \
|
||||
--hash=sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778 \
|
||||
--hash=sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1 \
|
||||
--hash=sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb \
|
||||
--hash=sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261 \
|
||||
--hash=sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb \
|
||||
--hash=sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a \
|
||||
--hash=sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8 \
|
||||
--hash=sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359 \
|
||||
--hash=sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5 \
|
||||
--hash=sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7 \
|
||||
--hash=sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751 \
|
||||
--hash=sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8 \
|
||||
--hash=sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605 \
|
||||
--hash=sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e \
|
||||
--hash=sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45 \
|
||||
--hash=sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2 \
|
||||
--hash=sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895 \
|
||||
--hash=sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe \
|
||||
--hash=sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb \
|
||||
--hash=sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a \
|
||||
--hash=sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577 \
|
||||
--hash=sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d \
|
||||
--hash=sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a \
|
||||
--hash=sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda \
|
||||
--hash=sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6 \
|
||||
--hash=sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20
|
||||
# via -r .github/requirements/ci-dev-py3.in
|
||||
packaging==26.2 \
|
||||
--hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \
|
||||
--hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661
|
||||
# via pytest
|
||||
pluggy==1.6.0 \
|
||||
--hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \
|
||||
--hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746
|
||||
# via pytest
|
||||
pygments==2.20.0 \
|
||||
--hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \
|
||||
--hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176
|
||||
# via pytest
|
||||
pytest==9.0.3 \
|
||||
--hash=sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9 \
|
||||
--hash=sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c
|
||||
# via -r .github/requirements/ci-dev-py3.in
|
||||
sortedcontainers==2.4.0 \
|
||||
--hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \
|
||||
--hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0
|
||||
# via hypothesis
|
||||
@@ -0,0 +1,8 @@
|
||||
# Python 3.9 dev/test tooling for the ci.yml binding test job.
|
||||
# numpy is capped <2.1 because that is the last series shipping cp39 wheels
|
||||
# (>=2.1 dropped Python 3.9). Locked output: ci-dev-py39.txt
|
||||
# Refresh via scripts/update-lockfiles.sh.
|
||||
maturin
|
||||
pytest
|
||||
numpy<2.1
|
||||
hypothesis
|
||||
@@ -0,0 +1,162 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# ./scripts/update-lockfiles.sh
|
||||
attrs==26.1.0 \
|
||||
--hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \
|
||||
--hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32
|
||||
# via hypothesis
|
||||
colorama==0.4.6 \
|
||||
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
|
||||
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
|
||||
# via pytest
|
||||
exceptiongroup==1.3.1 \
|
||||
--hash=sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219 \
|
||||
--hash=sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598
|
||||
# via
|
||||
# hypothesis
|
||||
# pytest
|
||||
hypothesis==6.141.1 \
|
||||
--hash=sha256:8ef356e1e18fbeaa8015aab3c805303b7fe4b868e5b506e87ad83c0bf951f46f \
|
||||
--hash=sha256:a5b3c39c16d98b7b4c3c5c8d4262e511e3b2255e6814ced8023af49087ad60b3
|
||||
# via -r .github/requirements/ci-dev-py39.in
|
||||
iniconfig==2.1.0 \
|
||||
--hash=sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7 \
|
||||
--hash=sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760
|
||||
# via pytest
|
||||
maturin==1.13.3 \
|
||||
--hash=sha256:0ef257e692cc756c87af5bea95ddfe7d3ac49d3376a7a87f728d63f06e7b6f8b \
|
||||
--hash=sha256:1cc0a110b224ca90406b668a3e3c1f5a515062e59e26292f6dbaf5fd4909c6f3 \
|
||||
--hash=sha256:2389fe92d017cea9d94e521fa0175314a4c52f79a1057b901fbc9f8686ef7d0b \
|
||||
--hash=sha256:3cc13929ca82aefa4adbf0f2c35419369796213c6fb0eb24e914945f50ef5d8c \
|
||||
--hash=sha256:3db93337ed97e60ffc878aa8b493cd7ae44d3a5e1a37256db3a4491f57565018 \
|
||||
--hash=sha256:4667ef609ab446c1b5e0bfe4f9fb99699ab6d8548433f8d1a684256e0b67217f \
|
||||
--hash=sha256:49fd6ab08da28098ccf37afca24cdba72376ba9c1eedf9dd25ff82ed771961ff \
|
||||
--hash=sha256:4cd478e6e4c56251e48ed079b8efd55b30bc5c09cf695a1bdafaeb582ee735a0 \
|
||||
--hash=sha256:53b08bd075649ce96513ad9abf241a43cb685ed6e9e7790f8dbc2d66e95d8323 \
|
||||
--hash=sha256:771e1e9e71a278e56db01552e0d1acfd1464259f9575b6e72842f893cd299079 \
|
||||
--hash=sha256:a2675e25f313034ae6f57388cf14818f87d8961c4a96795287f3e155f59beb11 \
|
||||
--hash=sha256:b6741d7bf4af97da937528fd1e523c6ab54f53d9a21870fa735d6e67fd88e273 \
|
||||
--hash=sha256:c00ea6428dea17bf616fe93770837634454b28c2de1a876e42ef8036c616079a \
|
||||
--hash=sha256:def4a435ea9d2ee93b18ba579dc8c9cf898889a66f312cd379b5e374ec3e3ad6
|
||||
# via -r .github/requirements/ci-dev-py39.in
|
||||
numpy==2.0.2 \
|
||||
--hash=sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a \
|
||||
--hash=sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195 \
|
||||
--hash=sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951 \
|
||||
--hash=sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1 \
|
||||
--hash=sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c \
|
||||
--hash=sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc \
|
||||
--hash=sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b \
|
||||
--hash=sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd \
|
||||
--hash=sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4 \
|
||||
--hash=sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd \
|
||||
--hash=sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318 \
|
||||
--hash=sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448 \
|
||||
--hash=sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece \
|
||||
--hash=sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d \
|
||||
--hash=sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5 \
|
||||
--hash=sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8 \
|
||||
--hash=sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57 \
|
||||
--hash=sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78 \
|
||||
--hash=sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66 \
|
||||
--hash=sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a \
|
||||
--hash=sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e \
|
||||
--hash=sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c \
|
||||
--hash=sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa \
|
||||
--hash=sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d \
|
||||
--hash=sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c \
|
||||
--hash=sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729 \
|
||||
--hash=sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97 \
|
||||
--hash=sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c \
|
||||
--hash=sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9 \
|
||||
--hash=sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669 \
|
||||
--hash=sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4 \
|
||||
--hash=sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73 \
|
||||
--hash=sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385 \
|
||||
--hash=sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8 \
|
||||
--hash=sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c \
|
||||
--hash=sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b \
|
||||
--hash=sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692 \
|
||||
--hash=sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15 \
|
||||
--hash=sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131 \
|
||||
--hash=sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a \
|
||||
--hash=sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326 \
|
||||
--hash=sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b \
|
||||
--hash=sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded \
|
||||
--hash=sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04 \
|
||||
--hash=sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd
|
||||
# via -r .github/requirements/ci-dev-py39.in
|
||||
packaging==26.2 \
|
||||
--hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \
|
||||
--hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661
|
||||
# via pytest
|
||||
pluggy==1.6.0 \
|
||||
--hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \
|
||||
--hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746
|
||||
# via pytest
|
||||
pygments==2.20.0 \
|
||||
--hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \
|
||||
--hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176
|
||||
# via pytest
|
||||
pytest==8.4.2 \
|
||||
--hash=sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01 \
|
||||
--hash=sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79
|
||||
# via -r .github/requirements/ci-dev-py39.in
|
||||
sortedcontainers==2.4.0 \
|
||||
--hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \
|
||||
--hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0
|
||||
# via hypothesis
|
||||
tomli==2.4.1 \
|
||||
--hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \
|
||||
--hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \
|
||||
--hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \
|
||||
--hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \
|
||||
--hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \
|
||||
--hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \
|
||||
--hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \
|
||||
--hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \
|
||||
--hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \
|
||||
--hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \
|
||||
--hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \
|
||||
--hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \
|
||||
--hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \
|
||||
--hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \
|
||||
--hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \
|
||||
--hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \
|
||||
--hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \
|
||||
--hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \
|
||||
--hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \
|
||||
--hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \
|
||||
--hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \
|
||||
--hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \
|
||||
--hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \
|
||||
--hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \
|
||||
--hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \
|
||||
--hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \
|
||||
--hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \
|
||||
--hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \
|
||||
--hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \
|
||||
--hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \
|
||||
--hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \
|
||||
--hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \
|
||||
--hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \
|
||||
--hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \
|
||||
--hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \
|
||||
--hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \
|
||||
--hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \
|
||||
--hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \
|
||||
--hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \
|
||||
--hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \
|
||||
--hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \
|
||||
--hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \
|
||||
--hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \
|
||||
--hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \
|
||||
--hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \
|
||||
--hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \
|
||||
--hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049
|
||||
# via
|
||||
# maturin
|
||||
# pytest
|
||||
typing-extensions==4.15.0 \
|
||||
--hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \
|
||||
--hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548
|
||||
# via exceptiongroup
|
||||
@@ -49,6 +49,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
|
||||
|
||||
@@ -62,6 +64,8 @@ jobs:
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
cache: pip
|
||||
cache-dependency-path: .github/requirements/bench.txt
|
||||
|
||||
- name: Wait before Python retry
|
||||
if: steps.setup_python.outcome == 'failure'
|
||||
@@ -75,11 +79,15 @@ jobs:
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
cache: pip
|
||||
cache-dependency-path: .github/requirements/bench.txt
|
||||
|
||||
- name: Install Python deps + peer libs
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install maturin numpy pandas talipp finta
|
||||
# Hash-locked deps (OpenSSF Scorecard PinnedDependencies). bench.yml
|
||||
# runs on a single Python version (3.11), so one lock file suffices.
|
||||
python -m pip install --require-hashes -r .github/requirements/bench.txt
|
||||
|
||||
- name: Build Wickra wheel
|
||||
working-directory: bindings/python
|
||||
|
||||
@@ -40,6 +40,8 @@ jobs:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
|
||||
@@ -88,6 +90,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node
|
||||
id: setup_node
|
||||
@@ -175,6 +179,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
|
||||
@@ -253,6 +259,8 @@ jobs:
|
||||
packages: "-p wickra-node"
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Rust ${{ matrix.toolchain }}
|
||||
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
|
||||
@@ -276,6 +284,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
|
||||
@@ -315,6 +325,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: cargo-deny
|
||||
uses: EmbarkStudios/cargo-deny-action@bb137d7af7e4fb67e5f82a49c4fce4fad40782fe # v2.0.20
|
||||
@@ -331,6 +343,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install nightly Rust
|
||||
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
|
||||
@@ -387,6 +401,8 @@ jobs:
|
||||
python-version: ["3.9", "3.11", "3.12", "3.13"]
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
|
||||
@@ -407,6 +423,8 @@ jobs:
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: pip
|
||||
cache-dependency-path: .github/requirements/ci-dev-*.txt
|
||||
|
||||
- name: Wait before Python retry
|
||||
if: steps.setup_python.outcome == 'failure'
|
||||
@@ -420,11 +438,21 @@ jobs:
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: pip
|
||||
cache-dependency-path: .github/requirements/ci-dev-*.txt
|
||||
|
||||
- name: Install Python dev dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install maturin pytest numpy hypothesis
|
||||
# Hash-locked dev tooling (OpenSSF Scorecard PinnedDependencies).
|
||||
# Split by Python version: numpy ships no single release with wheels
|
||||
# for both cp39 and cp313 (<=2.0.2 has cp39 only, >=2.1 drops cp39).
|
||||
if [ "${{ matrix.python-version }}" = "3.9" ]; then
|
||||
python -m pip install --require-hashes -r .github/requirements/ci-dev-py39.txt
|
||||
else
|
||||
python -m pip install --require-hashes -r .github/requirements/ci-dev-py3.txt
|
||||
fi
|
||||
|
||||
- name: Build wheel
|
||||
working-directory: bindings/python
|
||||
@@ -449,6 +477,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Rust toolchain (with wasm target)
|
||||
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
|
||||
@@ -495,6 +525,8 @@ jobs:
|
||||
node-version: ["18", "20"]
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
|
||||
@@ -514,6 +546,8 @@ jobs:
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: npm
|
||||
cache-dependency-path: bindings/node/package-lock.json
|
||||
|
||||
- name: Wait before Node retry
|
||||
if: steps.setup_node.outcome == 'failure'
|
||||
@@ -527,10 +561,12 @@ jobs:
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: npm
|
||||
cache-dependency-path: bindings/node/package-lock.json
|
||||
|
||||
- name: Install Node dependencies
|
||||
working-directory: bindings/node
|
||||
run: npm install
|
||||
run: npm ci
|
||||
|
||||
- name: Build native module
|
||||
working-directory: bindings/node
|
||||
|
||||
@@ -40,7 +40,9 @@ jobs:
|
||||
build-mode: none
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@03e4368ac7daa2bd82b3e85262f3bf87ee112f57 # v3.36.0
|
||||
|
||||
@@ -46,6 +46,8 @@ jobs:
|
||||
environment: release
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
|
||||
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
||||
continue-on-error: true # cache is an optimisation; never block on a stuck/slow restore
|
||||
@@ -156,6 +158,8 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python
|
||||
id: setup_python
|
||||
continue-on-error: true
|
||||
@@ -194,6 +198,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Sync root README into bindings/python so it ships in the sdist
|
||||
run: cp README.md bindings/python/README.md
|
||||
- uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
|
||||
@@ -244,6 +250,8 @@ jobs:
|
||||
runs-on: ${{ matrix.host }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node
|
||||
id: setup_node
|
||||
@@ -275,7 +283,7 @@ jobs:
|
||||
|
||||
- name: Install Node deps
|
||||
working-directory: bindings/node
|
||||
run: npm install
|
||||
run: npm ci
|
||||
|
||||
- name: Build native module
|
||||
working-directory: bindings/node
|
||||
@@ -303,6 +311,8 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node
|
||||
id: setup_node
|
||||
@@ -328,7 +338,7 @@ jobs:
|
||||
|
||||
- name: Install Node deps
|
||||
working-directory: bindings/node
|
||||
run: npm install
|
||||
run: npm ci
|
||||
|
||||
- name: Download all platform binaries
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
@@ -470,6 +480,8 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node
|
||||
id: setup_node
|
||||
@@ -570,13 +582,22 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Resolve target tag
|
||||
id: tag
|
||||
# Pass the (potentially attacker-influenceable on a tag push) ref context
|
||||
# through the environment instead of interpolating it into the shell
|
||||
# script, so a crafted tag name cannot inject commands (zizmor:
|
||||
# template-injection).
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
REF: ${{ github.ref }}
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "push" ] && [[ "${{ github.ref }}" == refs/tags/* ]]; then
|
||||
tag="${{ github.ref_name }}"
|
||||
if [ "$EVENT_NAME" = "push" ] && [[ "$REF" == refs/tags/* ]]; then
|
||||
tag="$REF_NAME"
|
||||
else
|
||||
# workflow_dispatch / non-tag push: attach to the latest v* tag.
|
||||
tag=$(git tag --list 'v*' --sort=-v:refname | head -n1)
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
id-token: write # OIDC token to publish results to the OpenSSF API
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ name: Sync indicator count
|
||||
# 3. Docs site: index.md / overview.md / Indicators-Overview.md
|
||||
# (wickra-lib/wickra-docs) — synced on push to main / v* tag*
|
||||
# 4. Marketing site count (wickra-lib/webpage: index.md /
|
||||
# .vitepress/config.ts / public/hero.svg) — push to main / v* tag*
|
||||
# .vitepress/config.ts) — push to main / v* tag*
|
||||
# 5. org profile README count (wickra-lib/.github, profile/README.md)
|
||||
# — synced on push to main / v* tag*
|
||||
# 6. org description ("… N indicators, install-free.")
|
||||
@@ -144,19 +144,20 @@ jobs:
|
||||
id: pr_check
|
||||
run: |
|
||||
n="${{ steps.count.outputs.count }}"
|
||||
if grep -qE "^${n} streaming-first indicators" README.md; then
|
||||
if grep -qE "^${n} streaming-first indicators" README.md \
|
||||
&& grep -qE "\*\*${n} indicators\*\*" docs/README.md; then
|
||||
echo "matches=true" >> "$GITHUB_OUTPUT"
|
||||
echo "README counter already at ${n}; nothing to do."
|
||||
echo "README + docs/README counter already at ${n}; nothing to do."
|
||||
else
|
||||
echo "matches=false" >> "$GITHUB_OUTPUT"
|
||||
echo "README counter does not match ${n}; will fix up."
|
||||
echo "README/docs counter does not match ${n}; will fix up."
|
||||
fi
|
||||
|
||||
- name: Fix counter on fork PR head (read-only, fail loud)
|
||||
if: github.event_name == 'pull_request' && steps.pr_check.outputs.matches == 'false' && steps.ctx.outputs.can_push == 'false'
|
||||
run: |
|
||||
n="${{ steps.count.outputs.count }}"
|
||||
echo "::error::README.md says a different indicator count than mod.rs (${n}). This PR is from a fork, so the workflow cannot push the fix; please update README.md to '${n} streaming-first indicators' and push again."
|
||||
echo "::error::README.md / docs/README.md say a different indicator count than lib.rs (${n}). This PR is from a fork, so the workflow cannot push the fix; please set README.md to '${n} streaming-first indicators' and docs/README.md to '**${n} indicators**', then push again."
|
||||
exit 1
|
||||
|
||||
- name: Patch README on PR head
|
||||
@@ -164,7 +165,13 @@ jobs:
|
||||
id: pr_patch
|
||||
run: |
|
||||
n="${{ steps.count.outputs.count }}"
|
||||
sed -i -E "s/[0-9]+ (streaming-first )?indicators/${n} \1indicators/g" README.md
|
||||
# docs/README.md carries the count in its docs.wickra.org pointer prose
|
||||
# ("**N indicators**"); keep it in sync with README's prose count.
|
||||
sed -i -E "s/[0-9]+ (streaming-first )?indicators/${n} \1indicators/g" README.md docs/README.md
|
||||
# Bump the banner cache-buster so GitHub's Camo proxy refetches the org
|
||||
# profile image (regenerated with the new count by .github/banner.yml)
|
||||
# instead of serving a stale cached copy. (README only — docs has no banner.)
|
||||
sed -i -E "s|(wickra-banner\.webp\?v=)[0-9]+|\1${n}|" README.md
|
||||
if git diff --quiet; then
|
||||
echo "No README changes after sed (counter regex did not match anything); skipping push."
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
@@ -183,7 +190,7 @@ jobs:
|
||||
run: |
|
||||
git config user.name "wickra-bot"
|
||||
git config user.email "wickra-bot@users.noreply.github.com"
|
||||
git add README.md
|
||||
git add README.md docs/README.md
|
||||
git commit -m "chore: sync indicator count to ${COUNT}"
|
||||
git push origin "HEAD:${HEAD_REF}"
|
||||
|
||||
@@ -424,14 +431,14 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
cd webpage-count
|
||||
sed -i -E "s/[0-9]+ (streaming-first )?indicators/${n} \1indicators/g" index.md .vitepress/config.ts public/hero.svg
|
||||
sed -i -E "s/[0-9]+ (streaming-first )?indicators/${n} \1indicators/g" index.md .vitepress/config.ts
|
||||
if git diff --quiet; then
|
||||
echo "Webpage indicator count unchanged."
|
||||
exit 0
|
||||
fi
|
||||
git config user.name "wickra-bot"
|
||||
git config user.email "wickra-bot@users.noreply.github.com"
|
||||
git add index.md .vitepress/config.ts public/hero.svg
|
||||
git add index.md .vitepress/config.ts
|
||||
git commit -m "chore: sync indicator count to ${n}"
|
||||
if ! git push 2>/dev/null; then
|
||||
echo "::warning::push to wickra-lib/webpage failed — ABOUT_SYNC_TOKEN likely lacks write (findings P10.0a)."
|
||||
|
||||
@@ -15,6 +15,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
name: zizmor
|
||||
|
||||
# Static analysis of the GitHub Actions workflows themselves — the surface the
|
||||
# CodeQL pass does not cover. zizmor flags template injection, overly broad
|
||||
# GITHUB_TOKEN permissions, unpinned actions, cache poisoning, and dangerous
|
||||
# triggers. Findings appear under Security -> Code scanning alongside CodeQL.
|
||||
#
|
||||
# Report-only: with `advanced-security: true` the action runs zizmor in SARIF
|
||||
# mode, which exits 0 regardless of findings, so this job never blocks CI —
|
||||
# triage happens in the Security tab. Switch to gating later (e.g. a
|
||||
# `min-severity` input) once the existing findings are triaged.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
schedule:
|
||||
- cron: '17 4 * * 1' # Mondays 04:17 UTC
|
||||
|
||||
# Least-privilege default for the auto-injected GITHUB_TOKEN; the job raises
|
||||
# exactly the scopes it needs below (matches codeql.yml's pattern).
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
zizmor:
|
||||
name: Audit workflows
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write # upload SARIF to code-scanning
|
||||
contents: read # checkout
|
||||
actions: read # online audits resolve referenced actions
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run zizmor
|
||||
uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
|
||||
@@ -0,0 +1,49 @@
|
||||
# zizmor configuration — https://docs.zizmor.sh/configuration/
|
||||
#
|
||||
# cache-poisoning (release.yml):
|
||||
# The release pipeline restores build caches (Swatinem/rust-cache for the Rust
|
||||
# compilation, actions/setup-node) as a deliberate, accepted optimisation.
|
||||
# zizmor flags these under cache-poisoning because release.yml publishes
|
||||
# artifacts to crates.io / PyPI / npm, so a poisoned cache could in theory
|
||||
# reach a released build. Our caches are maintainer-controlled and the
|
||||
# restore speedup is kept on purpose; we accept this risk rather than running
|
||||
# cache-free release builds. (Six of the eight hits are actions/setup-node,
|
||||
# which zizmor reports at "Low" confidence.)
|
||||
#
|
||||
# artipacked (sync-about.yml):
|
||||
# The sync-about job checks out with persisted credentials on purpose: it
|
||||
# pushes the indicator-count fix-up back to the PR head branch (git commit +
|
||||
# git push), which needs the token in the runner's git config. It uploads no
|
||||
# artifacts, so the persisted token is never packaged or leaked; accept it.
|
||||
#
|
||||
# template-injection (sync-about.yml):
|
||||
# False positive. Every flagged expansion is steps.count.outputs.count, the
|
||||
# indicator count produced by an internal `grep -c` over lib.rs. It is not
|
||||
# attacker-controllable, so there is nothing to inject.
|
||||
#
|
||||
# use-trusted-publishing (release.yml):
|
||||
# Informational suggestion to use OIDC trusted publishing for PyPI / npm
|
||||
# instead of long-lived tokens. A worthwhile migration, but it reconfigures
|
||||
# the live publish pipeline on the registry side; tracked separately rather
|
||||
# than blocking on it here.
|
||||
#
|
||||
# superfluous-actions (release.yml):
|
||||
# The GitHub release step uses softprops/action-gh-release. The runner ships
|
||||
# `gh`, so this is replaceable by a script step, but the action is stable and
|
||||
# battle-tested; we keep it deliberately.
|
||||
rules:
|
||||
cache-poisoning:
|
||||
ignore:
|
||||
- release.yml
|
||||
artipacked:
|
||||
ignore:
|
||||
- sync-about.yml
|
||||
template-injection:
|
||||
ignore:
|
||||
- sync-about.yml
|
||||
use-trusted-publishing:
|
||||
ignore:
|
||||
- release.yml
|
||||
superfluous-actions:
|
||||
ignore:
|
||||
- release.yml
|
||||
+179
-1
@@ -7,6 +7,181 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.4.4] - 2026-06-02
|
||||
|
||||
### Added
|
||||
- **TA-Lib candlestick patterns (part 1).** New candlestick pattern detectors
|
||||
matching TA-Lib `CDL*`, emitting the family's signed `+1 / 0 / −1` convention
|
||||
over OHLCV candles in Rust, Python, Node and WASM:
|
||||
- **Two Crows** — a three-bar bearish reversal (`CDL2CROWS`): a long white
|
||||
candle, a black candle whose body gaps up, then a black candle that opens
|
||||
inside the second's body and closes inside the first's.
|
||||
- **Upside Gap Two Crows** — a three-bar bearish reversal
|
||||
(`CDLUPSIDEGAP2CROWS`): two black candles gap up over a long white candle,
|
||||
the second engulfing the first crow yet still closing above the white body,
|
||||
leaving the upside gap open.
|
||||
- **Identical Three Crows** — a three-bar bearish reversal
|
||||
(`CDLIDENTICAL3CROWS`): three red candles with steadily lower closes, each
|
||||
opening at the prior candle's close so the bodies stack in an identical
|
||||
staircase.
|
||||
- **Three Line Strike** — a four-bar pattern (`CDL3LINESTRIKE`): a
|
||||
three-candle advance or decline struck by a fourth opposite-colour candle
|
||||
that engulfs the entire run; bullish `+1`, bearish `−1`.
|
||||
- **Three Stars in the South** — a rare three-bar bullish reversal
|
||||
(`CDL3STARSINSOUTH`): three shrinking red candles each carving a higher low
|
||||
and contracting toward a tiny black marubozu as selling exhausts.
|
||||
- **Abandoned Baby** — a strong three-bar reversal (`CDLABANDONEDBABY`): a doji
|
||||
isolated by price gaps on both sides; bullish `+1` after a decline, bearish
|
||||
`−1` after an advance.
|
||||
- **Advance Block** — a three-bar bearish warning (`CDLADVANCEBLOCK`): three
|
||||
green candles to higher closes whose bodies shrink as their upper shadows
|
||||
lengthen, signalling the advance is stalling.
|
||||
- **Belt-hold** — a single-bar reversal that opens at one extreme of its range and runs the other way; bullish +1, bearish -1 (`CDLBELTHOLD`).
|
||||
- **Breakaway** — a 5-bar reversal that gaps with the trend, drifts two more bars, then snaps back into the bar1/bar2 body gap; bullish +1, bearish -1 (`CDLBREAKAWAY`).
|
||||
- **Counterattack** — a 2-bar reversal where an opposite-coloured second bar closes level with the first (the counterattack line); bullish +1, bearish -1 (`CDLCOUNTERATTACK`).
|
||||
- **Doji Star** — a long body followed by a doji gapping away in the trend direction; bullish +1, bearish -1 (`CDLDOJISTAR`).
|
||||
- **Dragonfly Doji** — a doji opening and closing at the high with a long lower shadow, a bullish reversal; +1 (`CDLDRAGONFLYDOJI`).
|
||||
- **Gravestone Doji** — a doji opening and closing at the low with a long upper shadow, a bearish reversal; -1 (`CDLGRAVESTONEDOJI`).
|
||||
- **Long-Legged Doji** — a doji with long shadows on both sides, an indecision signal; +1 detection (`CDLLONGLEGGEDDOJI`).
|
||||
- **Rickshaw Man** — a long-legged doji with the body centred in the range, an indecision signal; +1 detection (`CDLRICKSHAWMAN`).
|
||||
- **Evening Doji Star** — a bearish top reversal: long white bar, a doji gapping up, then a black bar closing deep into the first body; -1 (`CDLEVENINGDOJISTAR`).
|
||||
- **Morning Doji Star** — a bullish bottom reversal: long black bar, a doji gapping down, then a white bar closing deep into the first body; +1 (`CDLMORNINGDOJISTAR`).
|
||||
- **Gap Side-by-Side White** — two similar white candles opening side by side after a gap, a continuation; gap up +1, gap down -1 (`CDLGAPSIDESIDEWHITE`).
|
||||
- **High-Wave** — a small body with very long shadows on both sides, an extreme indecision signal; +1 detection (`CDLHIGHWAVE`).
|
||||
- **Hikkake** — an inside bar followed by a failed breakout, a trap; bullish +1, bearish -1 (`CDLHIKKAKE`).
|
||||
- **Modified Hikkake** — a close-confirmed Hikkake: an inside bar then a failed breakout closing back inside; bullish +1, bearish -1 (`CDLHIKKAKEMOD`).
|
||||
- **Homing Pigeon** — two black candles, the second a small body inside the first, a bullish reversal; +1 (`CDLHOMINGPIGEON`).
|
||||
- **On-Neck** — a long black candle then a white candle closing at its low (the neckline), a bearish continuation; -1 (`CDLONNECK`).
|
||||
- **In-Neck** — a long black candle then a white candle closing just into its body, a bearish continuation; -1 (`CDLINNECK`).
|
||||
- **Thrusting** — a long black candle then a white candle closing well into but below the midpoint of its body, a bearish continuation; -1 (`CDLTHRUSTING`).
|
||||
- **Separating Lines** — opposite-coloured candles sharing the same open, the second an opening marubozu resuming the trend; bullish +1, bearish -1 (`CDLSEPARATINGLINES`).
|
||||
- **Kicking** — two opposite-coloured marubozu separated by a gap; bullish +1, bearish -1 (`CDLKICKING`).
|
||||
- **Kicking by Length** — a kicking pattern signalled by the colour of the longer marubozu; +1 / -1 (`CDLKICKINGBYLENGTH`).
|
||||
- **Ladder Bottom** — three descending black candles, a fourth with an upper shadow, then a white candle gapping up, a bullish reversal; +1 (`CDLLADDERBOTTOM`).
|
||||
- **Mat Hold** — a long white candle, a holding three-bar pullback, then a new-high white candle, a bullish continuation; +1 (`CDLMATHOLD`).
|
||||
- **Matching Low** — a 2-bar bullish reversal where two black candles in a decline share the same close, signalling selling pressure is exhausting; bullish +1 (`CDLMATCHINGLOW`).
|
||||
- **Long Line** — a single long-bodied candle with short shadows; bullish +1 (white) or bearish -1 (black) by colour (`CDLLONGLINE`).
|
||||
- **Short Line** — a single short-bodied candle with short shadows; bullish +1 (white) or bearish -1 (black) by colour (`CDLSHORTLINE`).
|
||||
- **Rising Three Methods** — a 5-bar bullish continuation: a long white candle, three small pullback bars holding within its range, then a white breakout to new highs; bullish +1 (`CDLRISEFALL3METHODS`).
|
||||
- **Falling Three Methods** — the bearish mirror of rising three methods: a long black candle, three small bars holding within its range, then a black breakdown to new lows; bearish -1 (`CDLRISEFALL3METHODS`).
|
||||
- **Upside Gap Three Methods** — a 3-bar bullish continuation: two white candles gap up, then a black candle opens within the second body and closes within the first; bullish +1 (`CDLXSIDEGAP3METHODS`).
|
||||
- **Downside Gap Three Methods** — the bearish mirror of upside gap three methods: two black candles gap down, then a white candle opens within the second body and closes within the first; bearish -1 (`CDLXSIDEGAP3METHODS`).
|
||||
- **Stalled Pattern** — a 3-bar bearish reversal warning: two long white candles then a small white candle riding the shoulder, signalling the rally is stalling; bearish -1 (`CDLSTALLEDPATTERN`).
|
||||
- **Stick Sandwich** — a 3-bar bullish reversal: two black candles closing at the same level sandwich a white candle, marking a support floor; bullish +1 (`CDLSTICKSANDWICH`).
|
||||
- **Takuri** — a single-bar bullish reversal, a strict Dragonfly Doji with a negligible upper shadow and very long lower shadow; bullish +1 (`CDLTAKURI`).
|
||||
- **Closing Marubozu** — a single long-bodied candle with no shadow on the close end; bullish +1 (white, closes at the high) or bearish -1 (black, closes at the low) (`CDLCLOSINGMARUBOZU`).
|
||||
- **Opening Marubozu** — a single long-bodied candle with no shadow on the open end; bullish +1 (white, opens at the low) or bearish -1 (black, opens at the high). No direct TA-Lib equivalent — completes the pair with the closing marubozu.
|
||||
- **Tasuki Gap** — a 3-bar continuation: two same-coloured candles gap in the trend direction, then an opposite candle opens within the second body and closes back into the gap without filling it; upside +1, downside -1 (`CDLTASUKIGAP`).
|
||||
- **Unique Three River** — a 3-bar bullish reversal: a long black candle, a black candle probing a new low with its body inside the first, then a small white candle held below it; bullish +1 (`CDLUNIQUE3RIVER`).
|
||||
- **Concealing Baby Swallow** — a rare 4-bar bullish capitulation: two black marubozu, a black candle gapping down with an upper shadow into the second, then a large black candle engulfing it entirely; bullish +1 (`CDLCONCEALBABYSWALL`).
|
||||
- **Derivatives family — funding & open interest (part 1).** A new family of
|
||||
indicators that consume a perpetual / futures tick (`DerivativesTick`,
|
||||
bundling funding rate, mark / index / futures price, open interest,
|
||||
positioning, taker flow and liquidations) rather than OHLCV, exposed in Rust,
|
||||
Python, Node and WASM:
|
||||
- **Funding Rate** — the current perpetual funding rate.
|
||||
- **Funding Rate Mean** — the rolling mean funding rate over a window.
|
||||
- **Funding Rate Z-Score** — the latest funding rate in standard deviations
|
||||
from its rolling mean.
|
||||
- **Funding Basis** — the perpetual's relative premium to spot,
|
||||
`(markPrice − indexPrice) / indexPrice`.
|
||||
- **Open-Interest Delta** — the tick-over-tick change in open interest.
|
||||
- **Derivatives family — open interest, flow & liquidations (part 2).** More
|
||||
indicators over the same `DerivativesTick` feed:
|
||||
- **OI / Price Divergence** — relative open-interest change minus relative
|
||||
price change over a window, the positioning-vs-price gap.
|
||||
- **OI-Weighted Price** — the cumulative mark price weighted by open interest.
|
||||
- **Long/Short Ratio** — aggregate long size over short size.
|
||||
- **Taker Buy/Sell Ratio** — taker buy volume over taker sell volume.
|
||||
- **Liquidation Features** — a multi-output breakdown of long/short
|
||||
liquidation notional into net, total and a bounded imbalance.
|
||||
- **Derivatives family — basis & term structure (part 3).** The final
|
||||
perpetual-vs-futures basis indicators over the `DerivativesTick` feed:
|
||||
- **Term-Structure Basis** — the dated future's relative premium to spot,
|
||||
`(futuresPrice − indexPrice) / indexPrice`.
|
||||
- **Calendar Spread** — the dated future's relative premium to the perpetual,
|
||||
`(futuresPrice − markPrice) / markPrice`.
|
||||
|
||||
## [0.4.3] - 2026-06-01
|
||||
|
||||
### Added
|
||||
- **Microstructure family — price impact & depth (part 3).** Indicators over a
|
||||
trade paired with the prevailing mid (`TradeQuote`) and over the order-book
|
||||
depth profile, exposed in Rust, Python, Node and WASM:
|
||||
- **Effective Spread** — `2 · D · (tradePrice − mid) / mid · 10_000` bps, the
|
||||
realised round-trip cost of a single trade against the mid.
|
||||
- **Realized Spread** — `2 · D · (tradePrice − mid_{t+horizon}) / mid_t ·
|
||||
10_000` bps, the share of the effective spread a liquidity provider keeps
|
||||
once the mid has moved over a configurable horizon.
|
||||
- **Kyle's Lambda** — the rolling OLS slope of mid changes on signed volume
|
||||
(`cov(Δmid, q) / var(q)`), the canonical price-impact / market-depth proxy.
|
||||
- **Depth Slope** — the mean per-side OLS slope of cumulative resting size
|
||||
against distance from the mid, measuring how fast the book thickens away
|
||||
from the touch.
|
||||
- **Microstructure family — footprint (part 4).** **Footprint** decomposes the
|
||||
volume traded in a bar across price buckets (`round(price / tick_size)`),
|
||||
splitting each bucket into buy-initiated (ask) and sell-initiated (bid)
|
||||
volume. A multi-output, variable-length indicator: every `update` returns the
|
||||
full footprint accumulated since the last `reset`, exposed in Rust, Python
|
||||
(`(k, 3)` arrays), Node (`{ price, bidVol, askVol }` rows) and WASM.
|
||||
|
||||
## [0.4.2] - 2026-06-01
|
||||
|
||||
### Added
|
||||
- **Microstructure family — order book (part 1).** A new family of indicators
|
||||
that consume an order-book depth snapshot (`OrderBook` of sorted, uncrossed
|
||||
bid/ask `Level`s) rather than OHLCV, exposed in Rust, Python, Node and WASM:
|
||||
- **Order-Book Imbalance** — `OrderBookImbalanceTop1`, `OrderBookImbalanceTopN`
|
||||
(configurable depth) and `OrderBookImbalanceFull` measure signed depth
|
||||
pressure `(bidDepth − askDepth) / (bidDepth + askDepth)` over the top level,
|
||||
the top-N levels, or the full book.
|
||||
- **Microprice** — the size-weighted fair value
|
||||
`(bidPx·askSz + askPx·bidSz) / (bidSz + askSz)`, tilting the mid toward the
|
||||
side more likely to be hit.
|
||||
- **Quoted Spread** — the top-of-book spread in basis points of the mid.
|
||||
- **Microstructure family — trade flow (part 2).** Indicators over a trade tape
|
||||
(`Trade` with an aggressor `Side`), exposed in Rust, Python, Node and WASM:
|
||||
- **Signed Volume** — per-trade size signed by aggressor side (`+size` buy,
|
||||
`−size` sell).
|
||||
- **Cumulative Volume Delta** — the running total of signed volume; reset to
|
||||
re-anchor per session.
|
||||
- **Trade Imbalance** — the rolling `(buyVol − sellVol)/(buyVol + sellVol)`
|
||||
over a configurable window of trades.
|
||||
|
||||
New public value types `Level`, `OrderBook`, `Side`, `Trade` and `TradeQuote`
|
||||
back this and the upcoming trade-flow and price-impact indicators. Python and
|
||||
Node accept a batch over a list of snapshots; WASM exposes per-snapshot
|
||||
`update`.
|
||||
- **Signed Doji encoding.** `Doji` gains an opt-in `.signed()` mode
|
||||
(`Doji(signed=True)` in Python, `new Doji(true)` in Node and WASM) that
|
||||
classifies a detected Doji by the position of its body within the bar range —
|
||||
a dragonfly (long lower shadow) emits `+1.0` (bullish), a gravestone (long
|
||||
upper shadow) emits `−1.0` (bearish), and a long-legged / standard Doji emits
|
||||
`0.0` (neutral). The default construction is unchanged — a direction-less
|
||||
`+1.0` / `0.0` detection flag — so existing callers are unaffected. This
|
||||
completes the uniform `+1` bull / `−1` bear / `0` none sign convention across
|
||||
every candlestick pattern, making the family a drop-in machine-learning
|
||||
feature where bullish and bearish instances share a single dimension.
|
||||
|
||||
### Fixed
|
||||
- **README banner now self-updates.** The top README banner points at the org
|
||||
profile image that `.github/banner.yml` regenerates from the indicator count,
|
||||
and `sync-about.yml` bumps a `?v=<count>` cache-buster so GitHub's Camo proxy
|
||||
refetches it immediately. Also fixes the webpage indicator-count sync, which
|
||||
silently crashed on a removed `public/hero.svg` and left the marketing site's
|
||||
count (and its OG banner) stale.
|
||||
|
||||
### Security
|
||||
- **CI dependency installs are pinned by hash.** The Node binding now installs
|
||||
with `npm ci` (strict `package-lock.json`), and the Python CI/bench tooling is
|
||||
installed from hash-locked `--require-hashes` requirements under
|
||||
`.github/requirements/` (OpenSSF Scorecard PinnedDependencies). The `ci-dev`
|
||||
tooling is locked twice — for Python 3.9 and for 3.10+ — because numpy ships no
|
||||
single release with wheels for both cp39 and cp313. A new
|
||||
`scripts/update-lockfiles.sh` regenerates every workspace lockfile (Rust, Node
|
||||
and the hash-pinned Python requirements) via `uv`, and Dependabot keeps the
|
||||
pinned requirements current.
|
||||
|
||||
## [0.4.1] - 2026-06-01
|
||||
|
||||
### Added
|
||||
@@ -900,7 +1075,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
optional Binance live feed.
|
||||
- Bindings for Python, Node.js, and WebAssembly.
|
||||
|
||||
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.4.1...HEAD
|
||||
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.4.4...HEAD
|
||||
[0.4.4]: https://github.com/wickra-lib/wickra/compare/v0.4.3...v0.4.4
|
||||
[0.4.3]: https://github.com/wickra-lib/wickra/compare/v0.4.2...v0.4.3
|
||||
[0.4.2]: https://github.com/wickra-lib/wickra/compare/v0.4.1...v0.4.2
|
||||
[0.4.1]: https://github.com/wickra-lib/wickra/compare/v0.4.0...v0.4.1
|
||||
[0.4.0]: https://github.com/wickra-lib/wickra/compare/v0.3.1...v0.4.0
|
||||
[0.3.1]: https://github.com/wickra-lib/wickra/compare/v0.3.0...v0.3.1
|
||||
|
||||
+10
-2
@@ -22,7 +22,7 @@ when proposing features or depending on Wickra elsewhere.
|
||||
| `bindings/node` | napi-rs bindings (`wickra` on npm). |
|
||||
| `bindings/wasm` | wasm-bindgen bindings (`wickra-wasm` on npm). |
|
||||
| `examples/` | Runnable examples. |
|
||||
| `docs/` | Pointer to the project Wiki, which holds all documentation. |
|
||||
| `docs/` | Pointer to the documentation site (docs.wickra.org); the docs live in the `wickra-lib/wickra-docs` repo. |
|
||||
|
||||
## Building and testing
|
||||
|
||||
@@ -75,7 +75,8 @@ wasm-pack test --node bindings/wasm
|
||||
| Workspace (Rust) | `Cargo.lock` | **yes** | The workspace ships binaries (examples, fuzz harness) and CI builds, so the dependency graph is pinned for reproducible builds. |
|
||||
| `bindings/node` | `package-lock.json` | **yes** | Reproducible `npm install` for the native binding. |
|
||||
| `examples/node` | `package-lock.json` | **yes** | Same — the runnable Node examples link the binding via a `file:` dependency. |
|
||||
| `bindings/python` | — | n/a (no lockfile) | PyO3 convention: the Python package has no Python runtime dependencies of its own, and its native code is already pinned through the workspace `Cargo.lock`. CI installs build/test tooling (`maturin`, `pytest`, `numpy`, `hypothesis`) directly via `pip`. |
|
||||
| `bindings/python` | — | n/a (no lockfile) | The published package pins only `numpy>=1.22` at runtime; its native code is pinned through the workspace `Cargo.lock`. The CI/bench dev tooling it installs is hash-locked separately — see the `.github/requirements` row. |
|
||||
| `.github/requirements` | `*.txt` (hash-pinned) | **yes** | CI/bench Python tooling, locked with `uv pip compile --generate-hashes` (OpenSSF Scorecard PinnedDependencies). `ci-dev` is split per Python version — `ci-dev-py39.txt` and `ci-dev-py3.txt` — because numpy ships no single release with wheels for both cp39 and cp313; `bench.txt` covers the single-version bench job. |
|
||||
| `fuzz` | `fuzz/Cargo.lock` | **no** (ignored) | `fuzz/` is a detached crate; `cargo-fuzz init` generates `fuzz/.gitignore` which ignores its `Cargo.lock`. The fuzz smoke job resolves dependencies fresh, so the lock is not needed for reproducibility here. |
|
||||
| `site` (marketing) | `package-lock.json` | **no** (ghost-ignored) | The VitePress site is a local-only project excluded via `.git/info/exclude`; its lockfile stays local. |
|
||||
|
||||
@@ -83,6 +84,13 @@ When adding a new committed Node package, commit its `package-lock.json` too and
|
||||
remove any matching ignore rule. Do **not** add a top-level `package-lock.json` —
|
||||
the repository root is not an npm package.
|
||||
|
||||
To refresh every committed lockfile in the workspace — `Cargo.lock`,
|
||||
`fuzz/Cargo.lock`, the Node binding lock, and the hash-pinned Python
|
||||
requirements — run `./scripts/update-lockfiles.sh`. It uses `uv` for the Python
|
||||
locks (and bootstraps it on Linux/macOS if absent) so each target Python
|
||||
version's hashed transitive closure can be regenerated without that interpreter
|
||||
installed. Dependabot also keeps the `.github/requirements` pins current.
|
||||
|
||||
## Standards for a change
|
||||
|
||||
- **Formatting & lints.** `cargo fmt` must leave the tree unchanged and
|
||||
|
||||
Generated
+6
-6
@@ -1867,7 +1867,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra"
|
||||
version = "0.4.1"
|
||||
version = "0.4.4"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"criterion",
|
||||
@@ -1878,7 +1878,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-core"
|
||||
version = "0.4.1"
|
||||
version = "0.4.4"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"proptest",
|
||||
@@ -1888,7 +1888,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-data"
|
||||
version = "0.4.1"
|
||||
version = "0.4.4"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"csv",
|
||||
@@ -1915,7 +1915,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-node"
|
||||
version = "0.4.1"
|
||||
version = "0.4.4"
|
||||
dependencies = [
|
||||
"napi",
|
||||
"napi-build",
|
||||
@@ -1925,7 +1925,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-python"
|
||||
version = "0.4.1"
|
||||
version = "0.4.4"
|
||||
dependencies = [
|
||||
"numpy",
|
||||
"pyo3",
|
||||
@@ -1934,7 +1934,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-wasm"
|
||||
version = "0.4.1"
|
||||
version = "0.4.4"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"js-sys",
|
||||
|
||||
+3
-3
@@ -12,11 +12,11 @@ members = [
|
||||
exclude = ["fuzz"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.4.1"
|
||||
version = "0.4.4"
|
||||
authors = ["kingchenc <support@wickra.org>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.86"
|
||||
license = "PolyForm-Noncommercial-1.0.0"
|
||||
license-file = "LICENSE"
|
||||
repository = "https://github.com/wickra-lib/wickra"
|
||||
homepage = "https://github.com/wickra-lib/wickra"
|
||||
readme = "README.md"
|
||||
@@ -24,7 +24,7 @@ keywords = ["finance", "trading", "indicators", "technical-analysis", "ta"]
|
||||
categories = ["finance", "mathematics", "science"]
|
||||
|
||||
[workspace.dependencies]
|
||||
wickra-core = { path = "crates/wickra-core", version = "0.4.1" }
|
||||
wickra-core = { path = "crates/wickra-core", version = "0.4.4" }
|
||||
|
||||
thiserror = "2"
|
||||
rayon = "1.10"
|
||||
|
||||
@@ -131,6 +131,31 @@ software under these terms.
|
||||
**Use** means anything you do with the software requiring one
|
||||
of your licenses.
|
||||
|
||||
## Additional Permissions Granted by the Licensor
|
||||
|
||||
These additional permissions supplement the PolyForm Noncommercial
|
||||
License 1.0.0 above. They only broaden, and never narrow, the
|
||||
licenses granted to you. The text of the PolyForm Noncommercial
|
||||
License 1.0.0 above is unmodified.
|
||||
|
||||
Use by a natural person, acting for their own personal account and
|
||||
not on behalf of any third party, is use for a permitted purpose.
|
||||
This includes operating an automated trading bot or trading strategy
|
||||
on that person's own capital, whether or not it earns that person
|
||||
money.
|
||||
|
||||
For the avoidance of doubt, the licenses above already let you use,
|
||||
fork, modify, and redistribute the software, and file issues and
|
||||
contribute changes, for any permitted purpose. Personal projects,
|
||||
research, education, nonprofit organizations, government use, and
|
||||
hobby trading bots are permitted purposes.
|
||||
|
||||
Any other commercial use — in particular the commercial sale of the
|
||||
software itself, or the commercial sale of services built around it —
|
||||
requires a separate commercial license from the licensor. If you want
|
||||
to use Wickra commercially, get in touch about a license at
|
||||
<https://github.com/wickra-lib/wickra>.
|
||||
|
||||
---
|
||||
|
||||
Required Notice: Copyright 2026 kingchenc (https://github.com/wickra-lib/wickra)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<p align="center">
|
||||
<a href="https://wickra.org"><img src="https://wickra.org/og-banner.webp" alt="Wickra — streaming-first technical indicators" width="100%"></a>
|
||||
<a href="https://wickra.org"><img src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/wickra-banner.webp?v=289" alt="Wickra — streaming-first technical indicators" width="100%"></a>
|
||||
</p>
|
||||
|
||||
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
|
||||
@@ -47,7 +47,7 @@ Full documentation lives at **[docs.wickra.org](https://docs.wickra.org)**:
|
||||
[Node](https://docs.wickra.org/Quickstart-Node),
|
||||
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
||||
- **Indicators** — a per-indicator deep dive (formula, parameters, warmup) for
|
||||
every one of the 219 indicators; start at the
|
||||
every one of the 289 indicators; start at the
|
||||
[indicators overview](https://docs.wickra.org/Indicators-Overview).
|
||||
- **Reference** — [warmup periods](https://docs.wickra.org/Warmup-Periods),
|
||||
[streaming vs batch](https://docs.wickra.org/Streaming-vs-Batch),
|
||||
@@ -135,7 +135,7 @@ python -m benchmarks.compare_libraries
|
||||
|
||||
## Indicators
|
||||
|
||||
219 streaming-first indicators across sixteen families. Every one passes the
|
||||
289 streaming-first indicators across eighteen families. Every one passes the
|
||||
`batch == streaming` equivalence test, reference-value tests, and reset
|
||||
semantics tests. Each has a per-indicator deep dive (formula, parameters,
|
||||
warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
||||
@@ -155,10 +155,18 @@ warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
||||
| Pivots & S/R | Classic Pivots, Fibonacci Pivots, Camarilla, Woodie Pivots, DeMark Pivots, Williams Fractals, ZigZag |
|
||||
| DeMark | TD Setup, TD Sequential, TD DeMarker, TD REI, TD Pressure, TD Combo, TD Countdown, TD Lines, TD Range Projection, TD Differential, TD Open, TD Risk Level |
|
||||
| Ichimoku & Charts | Ichimoku Kinko Hyo (Tenkan, Kijun, Senkou A/B, Chikou), Heikin-Ashi |
|
||||
| Candlestick Patterns | Doji, Hammer, Inverted Hammer, Hanging Man, Shooting Star, Engulfing, Harami, Morning/Evening Star, Three White Soldiers/Black Crows, Piercing Line/Dark Cloud Cover, Marubozu, Tweezer, Spinning Top, Three Inside Up/Down, Three Outside Up/Down |
|
||||
| Candlestick Patterns | Doji, Hammer, Inverted Hammer, Hanging Man, Shooting Star, Engulfing, Harami, Morning/Evening Star, Three White Soldiers/Black Crows, Piercing Line/Dark Cloud Cover, Marubozu, Tweezer, Spinning Top, Three Inside Up/Down, Three Outside Up/Down, Two Crows, Upside Gap Two Crows, Identical Three Crows, Three Line Strike, Three Stars in the South, Abandoned Baby, Advance Block, Belt-hold, Breakaway, Counterattack, Doji Star, Dragonfly Doji, Gravestone Doji, Long-Legged Doji, Rickshaw Man, Evening Doji Star, Morning Doji Star, Gap Side-by-Side White, High-Wave, Hikkake, Modified Hikkake, Homing Pigeon, On-Neck, In-Neck, Thrusting, Separating Lines, Kicking, Kicking by Length, Ladder Bottom, Mat Hold, Matching Low, Long Line, Short Line, Rising Three Methods, Falling Three Methods, Upside Gap Three Methods, Downside Gap Three Methods, Stalled Pattern, Stick Sandwich, Takuri, Closing Marubozu, Opening Marubozu, Tasuki Gap, Unique Three River, Concealing Baby Swallow |
|
||||
| Microstructure | Order-Book Imbalance (Top-1 / Top-N / Full), Microprice, Quoted Spread, Depth Slope, Signed Volume, Cumulative Volume Delta, Trade Imbalance, Effective Spread, Realized Spread, Kyle's Lambda, Footprint |
|
||||
| Derivatives | Funding Rate, Funding Rate Mean, Funding Rate Z-Score, Funding Basis, Open-Interest Delta, OI / Price Divergence, OI-Weighted Price, Long/Short Ratio, Taker Buy/Sell Ratio, Liquidation Features, Term-Structure Basis, Calendar Spread |
|
||||
| Market Profile | Value Area (POC / VAH / VAL), Initial Balance, Opening Range |
|
||||
| Risk / Performance | Sharpe Ratio, Sortino Ratio, Calmar Ratio, Omega Ratio, Max Drawdown, Average Drawdown, Drawdown Duration, Pain Index, Value at Risk, Conditional Value at Risk (CVaR), Profit Factor, Gain/Loss Ratio, Recovery Factor, Kelly Criterion, Treynor Ratio, Information Ratio, Alpha (Jensen) |
|
||||
|
||||
Every candlestick pattern emits a signed per-bar value — `+1.0` bullish,
|
||||
`−1.0` bearish, `0.0` none — so the family drops straight into a feature matrix
|
||||
as one column each. `Doji` is direction-less by default (`+1.0` / `0.0`);
|
||||
construct it in signed mode (`Doji::new().signed()`, `Doji(signed=True)`,
|
||||
`new Doji(true)`) for a dragonfly / gravestone `±1` reading.
|
||||
|
||||
Adding a new indicator means implementing one trait in Rust; all four bindings
|
||||
inherit it automatically.
|
||||
|
||||
@@ -230,7 +238,7 @@ A Python live-trading example using the public `websockets` package lives at
|
||||
```
|
||||
wickra/
|
||||
├── crates/
|
||||
│ ├── wickra-core/ core engine + all 219 indicators
|
||||
│ ├── wickra-core/ core engine + all 289 indicators
|
||||
│ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/
|
||||
│ └── wickra-data/ CSV reader, tick aggregator, live exchange feeds
|
||||
├── bindings/
|
||||
|
||||
@@ -9,7 +9,7 @@ edition.workspace = true
|
||||
# also emits `cargo::` directives that require >= 1.77 — that older floor is
|
||||
# subsumed by the 1.88 requirement now.
|
||||
rust-version = "1.88"
|
||||
license.workspace = true
|
||||
license-file.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
readme.workspace = true
|
||||
|
||||
@@ -235,6 +235,51 @@ const candleScalar = {
|
||||
SpinningTop: { make: () => new wickra.SpinningTop(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
ThreeInside: { make: () => new wickra.ThreeInside(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
ThreeOutside: { make: () => new wickra.ThreeOutside(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
TwoCrows: { make: () => new wickra.TwoCrows(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
UpsideGapTwoCrows: { make: () => new wickra.UpsideGapTwoCrows(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
IdenticalThreeCrows: { make: () => new wickra.IdenticalThreeCrows(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
ThreeLineStrike: { make: () => new wickra.ThreeLineStrike(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
ThreeStarsInSouth: { make: () => new wickra.ThreeStarsInSouth(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
AbandonedBaby: { make: () => new wickra.AbandonedBaby(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
AdvanceBlock: { make: () => new wickra.AdvanceBlock(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
BeltHold: { make: () => new wickra.BeltHold(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
Breakaway: { make: () => new wickra.Breakaway(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
Counterattack: { make: () => new wickra.Counterattack(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
DojiStar: { make: () => new wickra.DojiStar(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
DragonflyDoji: { make: () => new wickra.DragonflyDoji(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
GravestoneDoji: { make: () => new wickra.GravestoneDoji(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
LongLeggedDoji: { make: () => new wickra.LongLeggedDoji(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
RickshawMan: { make: () => new wickra.RickshawMan(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
EveningDojiStar: { make: () => new wickra.EveningDojiStar(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
MorningDojiStar: { make: () => new wickra.MorningDojiStar(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
GapSideBySideWhite: { make: () => new wickra.GapSideBySideWhite(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
HighWave: { make: () => new wickra.HighWave(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
Hikkake: { make: () => new wickra.Hikkake(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
HikkakeModified: { make: () => new wickra.HikkakeModified(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
HomingPigeon: { make: () => new wickra.HomingPigeon(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
OnNeck: { make: () => new wickra.OnNeck(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
InNeck: { make: () => new wickra.InNeck(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
Thrusting: { make: () => new wickra.Thrusting(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
SeparatingLines: { make: () => new wickra.SeparatingLines(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
Kicking: { make: () => new wickra.Kicking(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
KickingByLength: { make: () => new wickra.KickingByLength(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
LadderBottom: { make: () => new wickra.LadderBottom(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
MatHold: { make: () => new wickra.MatHold(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
MatchingLow: { make: () => new wickra.MatchingLow(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
LongLine: { make: () => new wickra.LongLine(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
ShortLine: { make: () => new wickra.ShortLine(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
RisingThreeMethods: { make: () => new wickra.RisingThreeMethods(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
FallingThreeMethods: { make: () => new wickra.FallingThreeMethods(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
UpsideGapThreeMethods: { make: () => new wickra.UpsideGapThreeMethods(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
DownsideGapThreeMethods: { make: () => new wickra.DownsideGapThreeMethods(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
StalledPattern: { make: () => new wickra.StalledPattern(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
StickSandwich: { make: () => new wickra.StickSandwich(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
Takuri: { make: () => new wickra.Takuri(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
ClosingMarubozu: { make: () => new wickra.ClosingMarubozu(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
OpeningMarubozu: { make: () => new wickra.OpeningMarubozu(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
TasukiGap: { make: () => new wickra.TasukiGap(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
UniqueThreeRiver: { make: () => new wickra.UniqueThreeRiver(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
ConcealingBabySwallow: { make: () => new wickra.ConcealingBabySwallow(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
};
|
||||
|
||||
for (const [name, d] of Object.entries(candleScalar)) {
|
||||
@@ -896,3 +941,307 @@ test('ALMA(3, 0.85, 6) reference value on [10, 20, 30]', () => {
|
||||
// simple mean of 20.
|
||||
assert.ok(out[2] > 20);
|
||||
});
|
||||
|
||||
test('Doji signed mode encodes dragonfly/gravestone/neutral direction', () => {
|
||||
// Default: direction-less detection flag (+1 doji / 0 otherwise).
|
||||
const flag = new wickra.Doji();
|
||||
assert.equal(flag.isSigned(), false);
|
||||
assert.equal(flag.update(10, 11, 9, 10), 1); // body 0, range 2 -> doji
|
||||
assert.equal(flag.update(10, 12, 10, 12), 0); // body == range -> not a doji
|
||||
|
||||
// Signed: classify a detected doji by its body position within the range.
|
||||
const d = new wickra.Doji(true);
|
||||
assert.equal(d.isSigned(), true);
|
||||
assert.equal(d.update(10, 10.05, 6, 10), 1); // dragonfly -> bullish +1
|
||||
assert.equal(d.update(10, 14, 9.95, 10), -1); // gravestone -> bearish -1
|
||||
assert.equal(d.update(10, 12, 8, 10), 0); // long-legged -> neutral 0
|
||||
assert.equal(d.update(10, 12, 10, 12), 0); // not a doji -> 0
|
||||
});
|
||||
|
||||
test('order-book indicators reference values', () => {
|
||||
// Top-1: (3 - 1) / (3 + 1) = 0.5.
|
||||
assert.equal(new wickra.OrderBookImbalanceTop1().update([100], [3], [101], [1]), 0.5);
|
||||
// Top-2: bidDepth 3, askDepth 2 -> (3 - 2) / 5 = 0.2.
|
||||
assert.ok(
|
||||
Math.abs(new wickra.OrderBookImbalanceTopN(2).update([100, 99], [2, 1], [101, 102], [1, 1]) - 0.2) < 1e-12,
|
||||
);
|
||||
// Full: bidDepth 1, askDepth 3 -> -0.5.
|
||||
assert.equal(new wickra.OrderBookImbalanceFull().update([100], [1], [101, 102], [2, 1]), -0.5);
|
||||
// Microprice: (100*3 + 101*1) / 4 = 100.25.
|
||||
assert.equal(new wickra.Microprice().update([100], [1], [101], [3]), 100.25);
|
||||
// Quoted spread: 1 / 100.5 * 10000 ≈ 99.5025 bps.
|
||||
assert.ok(Math.abs(new wickra.QuotedSpread().update([100], [1], [101], [1]) - 99.50248756) < 1e-6);
|
||||
// Depth slope: each side distances 1,2 -> cumulative 1,3 -> OLS slope 2.
|
||||
assert.ok(Math.abs(new wickra.DepthSlope().update([99, 98], [1, 2], [101, 102], [1, 2]) - 2.0) < 1e-9);
|
||||
// Single level per side -> no slope -> 0.
|
||||
assert.equal(new wickra.DepthSlope().update([100], [1], [101], [1]), 0.0);
|
||||
});
|
||||
|
||||
test('order-book streaming update matches batch', () => {
|
||||
const snaps = Array.from({ length: 30 }, (_, i) => ({
|
||||
bidPx: [100, 99],
|
||||
bidSz: [1 + (i % 5), 1],
|
||||
askPx: [101, 102],
|
||||
askSz: [1 + ((i + 1) % 3), 1],
|
||||
}));
|
||||
const batch = new wickra.Microprice().batch(snaps);
|
||||
const streamer = new wickra.Microprice();
|
||||
assert.equal(batch.length, snaps.length);
|
||||
for (let i = 0; i < snaps.length; i++) {
|
||||
const s = streamer.update(snaps[i].bidPx, snaps[i].bidSz, snaps[i].askPx, snaps[i].askSz);
|
||||
assert.ok(Math.abs(s - batch[i]) < 1e-12, `mismatch at ${i}: ${s} vs ${batch[i]}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('order-book TopN rejects zero levels', () => {
|
||||
assert.throws(() => new wickra.OrderBookImbalanceTopN(0));
|
||||
});
|
||||
|
||||
test('order-book update rejects a crossed book', () => {
|
||||
assert.throws(() => new wickra.QuotedSpread().update([102], [1], [101], [1]));
|
||||
});
|
||||
|
||||
test('trade-flow indicators reference values', () => {
|
||||
assert.equal(new wickra.SignedVolume().update(100, 2, true), 2);
|
||||
assert.equal(new wickra.SignedVolume().update(100, 3, false), -3);
|
||||
const cvd = new wickra.CumulativeVolumeDelta();
|
||||
assert.equal(cvd.update(100, 5, true), 5);
|
||||
assert.equal(cvd.update(100, 2, false), 3);
|
||||
const ti = new wickra.TradeImbalance(2);
|
||||
assert.equal(ti.update(100, 3, true), null); // warming up
|
||||
assert.equal(ti.update(100, 1, false), 0.5); // (3 - 1) / 4
|
||||
});
|
||||
|
||||
test('trade-flow streaming update matches batch', () => {
|
||||
const n = 30;
|
||||
const price = Array.from({ length: n }, () => 100);
|
||||
const size = Array.from({ length: n }, (_, i) => 1 + (i % 4));
|
||||
const isBuy = Array.from({ length: n }, (_, i) => i % 3 !== 0);
|
||||
const batch = new wickra.CumulativeVolumeDelta().batch(price, size, isBuy);
|
||||
const streamer = new wickra.CumulativeVolumeDelta();
|
||||
assert.equal(batch.length, n);
|
||||
for (let i = 0; i < n; i++) {
|
||||
const s = streamer.update(price[i], size[i], isBuy[i]);
|
||||
assert.ok(Math.abs(s - batch[i]) < 1e-12, `mismatch at ${i}: ${s} vs ${batch[i]}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('trade-flow rejects bad input', () => {
|
||||
assert.throws(() => new wickra.TradeImbalance(0));
|
||||
assert.throws(() => new wickra.SignedVolume().update(100, -1, true));
|
||||
});
|
||||
|
||||
test('price-impact indicators reference values', () => {
|
||||
// Buy at 100.05 vs mid 100.0: 2 * (100.05 - 100) / 100 * 10000 = 10 bps.
|
||||
assert.ok(Math.abs(new wickra.EffectiveSpread().update(100.05, 1, true, 100.0) - 10.0) < 1e-9);
|
||||
// Sell at 99.95 vs mid 100.0: 2 * -1 * (99.95 - 100) / 100 * 10000 = 10 bps.
|
||||
assert.ok(Math.abs(new wickra.EffectiveSpread().update(99.95, 1, false, 100.0) - 10.0) < 1e-9);
|
||||
// A buy filled below the mid is price improvement -> negative.
|
||||
assert.ok(new wickra.EffectiveSpread().update(99.95, 1, true, 100.0) < 0.0);
|
||||
});
|
||||
|
||||
test('price-impact streaming update matches batch', () => {
|
||||
const n = 30;
|
||||
const mid = Array.from({ length: n }, (_, i) => 100 + 0.25 * Math.sin(i * 0.5));
|
||||
const isBuy = Array.from({ length: n }, (_, i) => i % 3 !== 0);
|
||||
const price = Array.from({ length: n }, (_, i) => mid[i] + (isBuy[i] ? 0.03 : -0.03));
|
||||
const size = Array.from({ length: n }, (_, i) => 1 + (i % 4));
|
||||
const batch = new wickra.EffectiveSpread().batch(price, size, isBuy, mid);
|
||||
const streamer = new wickra.EffectiveSpread();
|
||||
assert.equal(batch.length, n);
|
||||
for (let i = 0; i < n; i++) {
|
||||
const s = streamer.update(price[i], size[i], isBuy[i], mid[i]);
|
||||
assert.ok(Math.abs(s - batch[i]) < 1e-9, `mismatch at ${i}: ${s} vs ${batch[i]}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('realized spread resolves against the future mid', () => {
|
||||
const rs = new wickra.RealizedSpread(1);
|
||||
assert.equal(rs.update(100.10, 1, true, 100.0), null); // buffered
|
||||
// 2 * (+1) * (100.10 - 100.20) / 100.0 * 10000 = -20 bps.
|
||||
assert.ok(Math.abs(rs.update(99.90, 1, false, 100.20) - -20.0) < 1e-9);
|
||||
});
|
||||
|
||||
test('realized spread streaming update matches batch', () => {
|
||||
const n = 30;
|
||||
const mid = Array.from({ length: n }, (_, i) => 100 + 0.25 * Math.sin(i * 0.5));
|
||||
const isBuy = Array.from({ length: n }, (_, i) => i % 3 !== 0);
|
||||
const price = Array.from({ length: n }, (_, i) => mid[i] + (isBuy[i] ? 0.03 : -0.03));
|
||||
const size = Array.from({ length: n }, (_, i) => 1 + (i % 4));
|
||||
const batch = new wickra.RealizedSpread(4).batch(price, size, isBuy, mid);
|
||||
const streamer = new wickra.RealizedSpread(4);
|
||||
assert.equal(batch.length, n);
|
||||
for (let i = 0; i < n; i++) {
|
||||
const s = streamer.update(price[i], size[i], isBuy[i], mid[i]);
|
||||
const got = s === null ? NaN : s;
|
||||
assert.ok(
|
||||
(Number.isNaN(got) && Number.isNaN(batch[i])) || Math.abs(got - batch[i]) < 1e-9,
|
||||
`mismatch at ${i}: ${got} vs ${batch[i]}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("kyle's lambda recovers a constant price-impact slope", () => {
|
||||
// Each trade moves the mid by exactly 0.5 per unit of signed volume.
|
||||
const impact = 0.5;
|
||||
let mid = 100;
|
||||
const price = [];
|
||||
const size = [];
|
||||
const isBuy = [];
|
||||
const mids = [];
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const buy = i % 2 === 0;
|
||||
const sz = 1 + (i % 3);
|
||||
const signed = buy ? sz : -sz;
|
||||
mid += impact * signed;
|
||||
price.push(mid);
|
||||
size.push(sz);
|
||||
isBuy.push(buy);
|
||||
mids.push(mid);
|
||||
}
|
||||
const out = new wickra.KylesLambda(6).batch(price, size, isBuy, mids);
|
||||
assert.ok(Math.abs(out[out.length - 1] - 0.5) < 1e-9);
|
||||
});
|
||||
|
||||
test('price-impact rejects bad input', () => {
|
||||
assert.throws(() => new wickra.EffectiveSpread().update(100, 1, true, 0));
|
||||
assert.throws(() => new wickra.RealizedSpread(0));
|
||||
assert.throws(() => new wickra.KylesLambda(1));
|
||||
});
|
||||
|
||||
test('footprint buckets buy and sell volume per price level', () => {
|
||||
const fp = new wickra.Footprint(1.0);
|
||||
fp.update(100.2, 2, true); // bucket 100 -> ask 2
|
||||
fp.update(100.7, 3, false); // bucket 101 -> bid 3
|
||||
const out = fp.update(100.1, 1, true); // bucket 100 -> ask 3
|
||||
assert.equal(out.length, 2);
|
||||
assert.deepEqual(
|
||||
{ price: out[0].price, bidVol: out[0].bidVol, askVol: out[0].askVol },
|
||||
{ price: 100.0, bidVol: 0.0, askVol: 3.0 },
|
||||
);
|
||||
assert.deepEqual(
|
||||
{ price: out[1].price, bidVol: out[1].bidVol, askVol: out[1].askVol },
|
||||
{ price: 101.0, bidVol: 3.0, askVol: 0.0 },
|
||||
);
|
||||
});
|
||||
|
||||
test('footprint streaming update matches batch and rejects bad tick', () => {
|
||||
const n = 12;
|
||||
const price = Array.from({ length: n }, (_, i) => 100 + (i % 5) * 0.3);
|
||||
const size = Array.from({ length: n }, (_, i) => 1 + (i % 3));
|
||||
const isBuy = Array.from({ length: n }, (_, i) => i % 2 === 0);
|
||||
const batch = new wickra.Footprint(1.0).batch(price, size, isBuy);
|
||||
const streamer = new wickra.Footprint(1.0);
|
||||
assert.equal(batch.length, n);
|
||||
for (let i = 0; i < n; i++) {
|
||||
const s = streamer.update(price[i], size[i], isBuy[i]);
|
||||
assert.deepEqual(s, batch[i], `mismatch at ${i}`);
|
||||
}
|
||||
assert.throws(() => new wickra.Footprint(0));
|
||||
});
|
||||
|
||||
test('derivatives indicators reference values', () => {
|
||||
// Funding rate passes through (and may be negative).
|
||||
assert.equal(new wickra.FundingRate().update(0.0001), 0.0001);
|
||||
assert.equal(new wickra.FundingRate().update(-0.0003), -0.0003);
|
||||
// Rolling mean: window [0.001, 0.003] -> 0.002.
|
||||
const frm = new wickra.FundingRateMean(2);
|
||||
assert.equal(frm.update(0.001), null); // warming up
|
||||
assert.ok(Math.abs(frm.update(0.003) - 0.002) < 1e-12);
|
||||
// Z-score: window [0.001, 0.003] -> +1.
|
||||
const z = new wickra.FundingRateZScore(2);
|
||||
assert.equal(z.update(0.001), null); // warming up
|
||||
assert.ok(Math.abs(z.update(0.003) - 1.0) < 1e-9);
|
||||
// Basis: mark 100.5 vs index 100.0 -> 0.005.
|
||||
assert.ok(Math.abs(new wickra.FundingBasis().update(100.5, 100.0) - 0.005) < 1e-12);
|
||||
// OI delta: seeds then emits the change.
|
||||
const oid = new wickra.OpenInterestDelta();
|
||||
assert.equal(oid.update(1000), null);
|
||||
assert.equal(oid.update(1250), 250);
|
||||
assert.equal(oid.update(1100), -150);
|
||||
});
|
||||
|
||||
test('derivatives streaming update matches batch', () => {
|
||||
const n = 30;
|
||||
const rate = Array.from({ length: n }, (_, i) => 0.0001 * Math.sin(i * 0.3));
|
||||
const batch = new wickra.FundingRateMean(5).batch(rate);
|
||||
const streamer = new wickra.FundingRateMean(5);
|
||||
assert.equal(batch.length, n);
|
||||
for (let i = 0; i < n; i++) {
|
||||
const s = streamer.update(rate[i]);
|
||||
assert.ok(
|
||||
(s === null && Number.isNaN(batch[i])) || Math.abs(s - batch[i]) < 1e-12,
|
||||
`mismatch at ${i}: ${s} vs ${batch[i]}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('derivatives reject bad input', () => {
|
||||
assert.throws(() => new wickra.FundingRateMean(0));
|
||||
assert.throws(() => new wickra.FundingRateZScore(0));
|
||||
assert.throws(() => new wickra.FundingBasis().update(100, 0));
|
||||
});
|
||||
|
||||
test('OI / flow / liquidation indicators reference values', () => {
|
||||
// OI +10% while price flat -> divergence +0.1.
|
||||
const div = new wickra.OIPriceDivergence(1);
|
||||
assert.equal(div.update(1000, 100), null); // warming up
|
||||
assert.ok(Math.abs(div.update(1100, 100) - 0.1) < 1e-12);
|
||||
// OI-weighted: (100·10 + 110·30) / 40 = 107.5.
|
||||
const oiw = new wickra.OIWeighted();
|
||||
assert.equal(oiw.update(100, 10), 100);
|
||||
assert.ok(Math.abs(oiw.update(110, 30) - 107.5) < 1e-12);
|
||||
// Long/short ratio.
|
||||
assert.ok(Math.abs(new wickra.LongShortRatio().update(600, 400) - 1.5) < 1e-12);
|
||||
assert.equal(new wickra.LongShortRatio().update(600, 0), 0);
|
||||
// Taker buy/sell ratio.
|
||||
assert.ok(Math.abs(new wickra.TakerBuySellRatio().update(60, 40) - 1.5) < 1e-12);
|
||||
assert.equal(new wickra.TakerBuySellRatio().update(60, 0), 0);
|
||||
// Liquidation features object.
|
||||
const liq = new wickra.LiquidationFeatures().update(30, 10);
|
||||
assert.equal(liq.net, 20);
|
||||
assert.equal(liq.total, 40);
|
||||
assert.equal(liq.imbalance, 0.5);
|
||||
});
|
||||
|
||||
test('liquidation features batch is flat n*5', () => {
|
||||
const longLiq = [10, 0, 30];
|
||||
const shortLiq = [5, 20, 0];
|
||||
const batch = new wickra.LiquidationFeatures().batch(longLiq, shortLiq);
|
||||
assert.equal(batch.length, 15);
|
||||
// Row 0: long 10, short 5, net 5, total 15.
|
||||
assert.equal(batch[0], 10);
|
||||
assert.equal(batch[1], 5);
|
||||
assert.equal(batch[2], 5);
|
||||
assert.equal(batch[3], 15);
|
||||
});
|
||||
|
||||
test('OI flow rejects bad input', () => {
|
||||
assert.throws(() => new wickra.OIPriceDivergence(0));
|
||||
assert.throws(() => new wickra.OIWeighted().update(0, 100));
|
||||
});
|
||||
|
||||
test('basis & calendar-spread reference values', () => {
|
||||
// futures 102 vs index 100 -> 0.02 contango.
|
||||
assert.ok(Math.abs(new wickra.TermStructureBasis().update(102, 100) - 0.02) < 1e-12);
|
||||
assert.ok(Math.abs(new wickra.TermStructureBasis().update(98, 100) + 0.02) < 1e-12);
|
||||
// futures 101 vs perpetual mark 100 -> 0.01.
|
||||
assert.ok(Math.abs(new wickra.CalendarSpread().update(101, 100) - 0.01) < 1e-12);
|
||||
});
|
||||
|
||||
test('basis streaming update matches batch', () => {
|
||||
const n = 20;
|
||||
const index = Array.from({ length: n }, (_, i) => 100 + Math.sin(i * 0.2));
|
||||
const futures = Array.from({ length: n }, (_, i) => index[i] + 0.5);
|
||||
const batch = new wickra.TermStructureBasis().batch(futures, index);
|
||||
const streamer = new wickra.TermStructureBasis();
|
||||
assert.equal(batch.length, n);
|
||||
for (let i = 0; i < n; i++) {
|
||||
assert.ok(Math.abs(streamer.update(futures[i], index[i]) - batch[i]) < 1e-12);
|
||||
}
|
||||
});
|
||||
|
||||
test('basis rejects bad input', () => {
|
||||
assert.throws(() => new wickra.TermStructureBasis().update(100, 0));
|
||||
assert.throws(() => new wickra.CalendarSpread().update(100, 0));
|
||||
});
|
||||
|
||||
Vendored
+653
-1
@@ -279,6 +279,27 @@ export interface OpeningRangeValue {
|
||||
low: number
|
||||
breakoutDistance: number
|
||||
}
|
||||
/** One order-book depth snapshot for batch evaluation. */
|
||||
export interface ObSnapshot {
|
||||
bidPx: Array<number>
|
||||
bidSz: Array<number>
|
||||
askPx: Array<number>
|
||||
askSz: Array<number>
|
||||
}
|
||||
/** One price bucket of a footprint. */
|
||||
export interface FootprintLevelValue {
|
||||
price: number
|
||||
bidVol: number
|
||||
askVol: number
|
||||
}
|
||||
/** The liquidation feature vector for one tick. */
|
||||
export interface LiquidationFeaturesValue {
|
||||
long: number
|
||||
short: number
|
||||
net: number
|
||||
total: number
|
||||
imbalance: number
|
||||
}
|
||||
export type SmaNode = SMA
|
||||
export declare class SMA {
|
||||
constructor(period: number)
|
||||
@@ -2055,12 +2076,13 @@ export declare class OpeningRange {
|
||||
}
|
||||
export type DojiNode = Doji
|
||||
export declare class Doji {
|
||||
constructor()
|
||||
constructor(signed?: boolean | undefined | null)
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
isSigned(): boolean
|
||||
}
|
||||
export type HammerNode = Hammer
|
||||
export declare class Hammer {
|
||||
@@ -2188,6 +2210,636 @@ export declare class ThreeOutside {
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type TwoCrowsNode = TwoCrows
|
||||
export declare class TwoCrows {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type UpsideGapTwoCrowsNode = UpsideGapTwoCrows
|
||||
export declare class UpsideGapTwoCrows {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type IdenticalThreeCrowsNode = IdenticalThreeCrows
|
||||
export declare class IdenticalThreeCrows {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type ThreeLineStrikeNode = ThreeLineStrike
|
||||
export declare class ThreeLineStrike {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type ThreeStarsInSouthNode = ThreeStarsInSouth
|
||||
export declare class ThreeStarsInSouth {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type AbandonedBabyNode = AbandonedBaby
|
||||
export declare class AbandonedBaby {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type AdvanceBlockNode = AdvanceBlock
|
||||
export declare class AdvanceBlock {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type BeltHoldNode = BeltHold
|
||||
export declare class BeltHold {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type BreakawayNode = Breakaway
|
||||
export declare class Breakaway {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type CounterattackNode = Counterattack
|
||||
export declare class Counterattack {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type DojiStarNode = DojiStar
|
||||
export declare class DojiStar {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type DragonflyDojiNode = DragonflyDoji
|
||||
export declare class DragonflyDoji {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type GravestoneDojiNode = GravestoneDoji
|
||||
export declare class GravestoneDoji {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type LongLeggedDojiNode = LongLeggedDoji
|
||||
export declare class LongLeggedDoji {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type RickshawManNode = RickshawMan
|
||||
export declare class RickshawMan {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type EveningDojiStarNode = EveningDojiStar
|
||||
export declare class EveningDojiStar {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type MorningDojiStarNode = MorningDojiStar
|
||||
export declare class MorningDojiStar {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type GapSideBySideWhiteNode = GapSideBySideWhite
|
||||
export declare class GapSideBySideWhite {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type HighWaveNode = HighWave
|
||||
export declare class HighWave {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type HikkakeNode = Hikkake
|
||||
export declare class Hikkake {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type HikkakeModifiedNode = HikkakeModified
|
||||
export declare class HikkakeModified {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type HomingPigeonNode = HomingPigeon
|
||||
export declare class HomingPigeon {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type OnNeckNode = OnNeck
|
||||
export declare class OnNeck {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type InNeckNode = InNeck
|
||||
export declare class InNeck {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type ThrustingNode = Thrusting
|
||||
export declare class Thrusting {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type SeparatingLinesNode = SeparatingLines
|
||||
export declare class SeparatingLines {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type KickingNode = Kicking
|
||||
export declare class Kicking {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type KickingByLengthNode = KickingByLength
|
||||
export declare class KickingByLength {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type LadderBottomNode = LadderBottom
|
||||
export declare class LadderBottom {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type MatHoldNode = MatHold
|
||||
export declare class MatHold {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type MatchingLowNode = MatchingLow
|
||||
export declare class MatchingLow {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type LongLineNode = LongLine
|
||||
export declare class LongLine {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type ShortLineNode = ShortLine
|
||||
export declare class ShortLine {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type RisingThreeMethodsNode = RisingThreeMethods
|
||||
export declare class RisingThreeMethods {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type FallingThreeMethodsNode = FallingThreeMethods
|
||||
export declare class FallingThreeMethods {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type UpsideGapThreeMethodsNode = UpsideGapThreeMethods
|
||||
export declare class UpsideGapThreeMethods {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type DownsideGapThreeMethodsNode = DownsideGapThreeMethods
|
||||
export declare class DownsideGapThreeMethods {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type StalledPatternNode = StalledPattern
|
||||
export declare class StalledPattern {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type StickSandwichNode = StickSandwich
|
||||
export declare class StickSandwich {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type TakuriNode = Takuri
|
||||
export declare class Takuri {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type ClosingMarubozuNode = ClosingMarubozu
|
||||
export declare class ClosingMarubozu {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type OpeningMarubozuNode = OpeningMarubozu
|
||||
export declare class OpeningMarubozu {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type TasukiGapNode = TasukiGap
|
||||
export declare class TasukiGap {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type UniqueThreeRiverNode = UniqueThreeRiver
|
||||
export declare class UniqueThreeRiver {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type ConcealingBabySwallowNode = ConcealingBabySwallow
|
||||
export declare class ConcealingBabySwallow {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type OrderBookImbalanceTop1Node = OrderBookImbalanceTop1
|
||||
export declare class OrderBookImbalanceTop1 {
|
||||
constructor()
|
||||
update(bidPx: Array<number>, bidSz: Array<number>, askPx: Array<number>, askSz: Array<number>): number | null
|
||||
batch(snapshots: Array<ObSnapshot>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type OrderBookImbalanceFullNode = OrderBookImbalanceFull
|
||||
export declare class OrderBookImbalanceFull {
|
||||
constructor()
|
||||
update(bidPx: Array<number>, bidSz: Array<number>, askPx: Array<number>, askSz: Array<number>): number | null
|
||||
batch(snapshots: Array<ObSnapshot>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type MicropriceNode = Microprice
|
||||
export declare class Microprice {
|
||||
constructor()
|
||||
update(bidPx: Array<number>, bidSz: Array<number>, askPx: Array<number>, askSz: Array<number>): number | null
|
||||
batch(snapshots: Array<ObSnapshot>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type QuotedSpreadNode = QuotedSpread
|
||||
export declare class QuotedSpread {
|
||||
constructor()
|
||||
update(bidPx: Array<number>, bidSz: Array<number>, askPx: Array<number>, askSz: Array<number>): number | null
|
||||
batch(snapshots: Array<ObSnapshot>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type DepthSlopeNode = DepthSlope
|
||||
export declare class DepthSlope {
|
||||
constructor()
|
||||
update(bidPx: Array<number>, bidSz: Array<number>, askPx: Array<number>, askSz: Array<number>): number | null
|
||||
batch(snapshots: Array<ObSnapshot>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type OrderBookImbalanceTopNNode = OrderBookImbalanceTopN
|
||||
export declare class OrderBookImbalanceTopN {
|
||||
constructor(levels: number)
|
||||
update(bidPx: Array<number>, bidSz: Array<number>, askPx: Array<number>, askSz: Array<number>): number | null
|
||||
batch(snapshots: Array<ObSnapshot>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type SignedVolumeNode = SignedVolume
|
||||
export declare class SignedVolume {
|
||||
constructor()
|
||||
update(price: number, size: number, isBuy: boolean): number | null
|
||||
batch(price: Array<number>, size: Array<number>, isBuy: Array<boolean>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type CumulativeVolumeDeltaNode = CumulativeVolumeDelta
|
||||
export declare class CumulativeVolumeDelta {
|
||||
constructor()
|
||||
update(price: number, size: number, isBuy: boolean): number | null
|
||||
batch(price: Array<number>, size: Array<number>, isBuy: Array<boolean>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type TradeImbalanceNode = TradeImbalance
|
||||
export declare class TradeImbalance {
|
||||
constructor(window: number)
|
||||
update(price: number, size: number, isBuy: boolean): number | null
|
||||
batch(price: Array<number>, size: Array<number>, isBuy: Array<boolean>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type EffectiveSpreadNode = EffectiveSpread
|
||||
export declare class EffectiveSpread {
|
||||
constructor()
|
||||
update(price: number, size: number, isBuy: boolean, mid: number): number | null
|
||||
batch(price: Array<number>, size: Array<number>, isBuy: Array<boolean>, mid: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type RealizedSpreadNode = RealizedSpread
|
||||
export declare class RealizedSpread {
|
||||
constructor(horizon: number)
|
||||
update(price: number, size: number, isBuy: boolean, mid: number): number | null
|
||||
batch(price: Array<number>, size: Array<number>, isBuy: Array<boolean>, mid: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type KylesLambdaNode = KylesLambda
|
||||
export declare class KylesLambda {
|
||||
constructor(window: number)
|
||||
update(price: number, size: number, isBuy: boolean, mid: number): number | null
|
||||
batch(price: Array<number>, size: Array<number>, isBuy: Array<boolean>, mid: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type FootprintNode = Footprint
|
||||
export declare class Footprint {
|
||||
constructor(tickSize: number)
|
||||
update(price: number, size: number, isBuy: boolean): Array<FootprintLevelValue>
|
||||
batch(price: Array<number>, size: Array<number>, isBuy: Array<boolean>): Array<Array<FootprintLevelValue>>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type FundingRateNode = FundingRate
|
||||
export declare class FundingRate {
|
||||
constructor()
|
||||
update(fundingRate: number): number | null
|
||||
batch(fundingRate: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type FundingRateMeanNode = FundingRateMean
|
||||
export declare class FundingRateMean {
|
||||
constructor(window: number)
|
||||
update(fundingRate: number): number | null
|
||||
batch(fundingRate: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type FundingRateZScoreNode = FundingRateZScore
|
||||
export declare class FundingRateZScore {
|
||||
constructor(window: number)
|
||||
update(fundingRate: number): number | null
|
||||
batch(fundingRate: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type FundingBasisNode = FundingBasis
|
||||
export declare class FundingBasis {
|
||||
constructor()
|
||||
update(markPrice: number, indexPrice: number): number | null
|
||||
batch(markPrice: Array<number>, indexPrice: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type OpenInterestDeltaNode = OpenInterestDelta
|
||||
export declare class OpenInterestDelta {
|
||||
constructor()
|
||||
update(openInterest: number): number | null
|
||||
batch(openInterest: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type OIPriceDivergenceNode = OIPriceDivergence
|
||||
export declare class OIPriceDivergence {
|
||||
constructor(window: number)
|
||||
update(openInterest: number, markPrice: number): number | null
|
||||
batch(openInterest: Array<number>, markPrice: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type OIWeightedNode = OIWeighted
|
||||
export declare class OIWeighted {
|
||||
constructor()
|
||||
update(markPrice: number, openInterest: number): number | null
|
||||
batch(markPrice: Array<number>, openInterest: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type LongShortRatioNode = LongShortRatio
|
||||
export declare class LongShortRatio {
|
||||
constructor()
|
||||
update(longSize: number, shortSize: number): number | null
|
||||
batch(longSize: Array<number>, shortSize: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type TakerBuySellRatioNode = TakerBuySellRatio
|
||||
export declare class TakerBuySellRatio {
|
||||
constructor()
|
||||
update(takerBuyVolume: number, takerSellVolume: number): number | null
|
||||
batch(takerBuyVolume: Array<number>, takerSellVolume: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type LiquidationFeaturesNode = LiquidationFeatures
|
||||
export declare class LiquidationFeatures {
|
||||
constructor()
|
||||
update(longLiquidation: number, shortLiquidation: number): LiquidationFeaturesValue | null
|
||||
batch(longLiquidation: Array<number>, shortLiquidation: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type TermStructureBasisNode = TermStructureBasis
|
||||
export declare class TermStructureBasis {
|
||||
constructor()
|
||||
update(futuresPrice: number, indexPrice: number): number | null
|
||||
batch(futuresPrice: Array<number>, indexPrice: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type CalendarSpreadNode = CalendarSpread
|
||||
export declare class CalendarSpread {
|
||||
constructor()
|
||||
update(futuresPrice: number, markPrice: number): number | null
|
||||
batch(futuresPrice: Array<number>, markPrice: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type SharpeRatioNode = SharpeRatio
|
||||
export declare class SharpeRatio {
|
||||
constructor(period: number, riskFree: number)
|
||||
|
||||
+71
-1
@@ -310,7 +310,7 @@ if (!nativeBinding) {
|
||||
throw new Error(`Failed to load native binding`)
|
||||
}
|
||||
|
||||
const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, McGinleyDynamic, FRAMA, SuperSmoother, FisherTransform, Decycler, CenterOfGravity, CyberneticCycle, InstantaneousTrendline, EhlersStochastic, RVIVolatility, Variance, CoefficientOfVariation, Skewness, Kurtosis, StandardError, DetrendedStdDev, RSquared, MedianAbsoluteDeviation, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpearmanCorrelation, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration, RelativeStrengthAB, MACD, BollingerBands, ATR, Stochastic, OBV, ADX, ADXR, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP, AwesomeOscillator, Aroon, Inertia, ConnorsRSI, LaguerreRSI, SMI, KST, PGO, RVI, AwesomeOscillatorHistogram, STC, ElderImpulse, ZeroLagMACD, CFO, APO, KAMA, EVWMA, Alligator, JMA, VIDYA, ALMA, T3, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, NVI, PVI, VolumeOscillator, KVO, WilliamsAD, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, EaseOfMovement, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, HiLoActivator, VoltyStop, YoyoExit, DonchianStop, PercentageTrailingStop, StepTrailingStop, RenkoTrailingStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, AcceleratorOscillator, BalanceOfPower, ChoppinessIndex, TrueRange, ChaikinVolatility, YangZhangVolatility, RogersSatchellVolatility, GarmanKlassVolatility, ParkinsonVolatility, LinRegAngle, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, WaveTrend, RWI, Vortex, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA, MaEnvelope, AccelerationBands, StarcBands, AtrBands, HurstChannel, LinRegChannel, StandardErrorBands, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands, ClassicPivots, FibonacciPivots, Camarilla, WoodiePivots, DemarkPivots, WilliamsFractals, ZigZag, TDSetup, TDSequential, TDDeMarker, TDREI, TDPressure, TDCombo, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, InverseFisherTransform, DecyclerOscillator, RoofingFilter, EmpiricalModeDecomposition, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, ValueArea, InitialBalance, OpeningRange, Doji, Hammer, InvertedHammer, HangingMan, ShootingStar, Engulfing, Harami, MorningEveningStar, ThreeSoldiersOrCrows, PiercingDarkCloud, Marubozu, Tweezer, SpinningTop, ThreeInside, ThreeOutside, SharpeRatio, SortinoRatio, CalmarRatio, OmegaRatio, MaxDrawdown, AverageDrawdown, DrawdownDuration, PainIndex, ValueAtRisk, ConditionalValueAtRisk, ProfitFactor, GainLossRatio, RecoveryFactor, KellyCriterion, TreynorRatio, InformationRatio, Alpha } = nativeBinding
|
||||
const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, McGinleyDynamic, FRAMA, SuperSmoother, FisherTransform, Decycler, CenterOfGravity, CyberneticCycle, InstantaneousTrendline, EhlersStochastic, RVIVolatility, Variance, CoefficientOfVariation, Skewness, Kurtosis, StandardError, DetrendedStdDev, RSquared, MedianAbsoluteDeviation, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpearmanCorrelation, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration, RelativeStrengthAB, MACD, BollingerBands, ATR, Stochastic, OBV, ADX, ADXR, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP, AwesomeOscillator, Aroon, Inertia, ConnorsRSI, LaguerreRSI, SMI, KST, PGO, RVI, AwesomeOscillatorHistogram, STC, ElderImpulse, ZeroLagMACD, CFO, APO, KAMA, EVWMA, Alligator, JMA, VIDYA, ALMA, T3, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, NVI, PVI, VolumeOscillator, KVO, WilliamsAD, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, EaseOfMovement, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, HiLoActivator, VoltyStop, YoyoExit, DonchianStop, PercentageTrailingStop, StepTrailingStop, RenkoTrailingStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, AcceleratorOscillator, BalanceOfPower, ChoppinessIndex, TrueRange, ChaikinVolatility, YangZhangVolatility, RogersSatchellVolatility, GarmanKlassVolatility, ParkinsonVolatility, LinRegAngle, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, WaveTrend, RWI, Vortex, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA, MaEnvelope, AccelerationBands, StarcBands, AtrBands, HurstChannel, LinRegChannel, StandardErrorBands, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands, ClassicPivots, FibonacciPivots, Camarilla, WoodiePivots, DemarkPivots, WilliamsFractals, ZigZag, TDSetup, TDSequential, TDDeMarker, TDREI, TDPressure, TDCombo, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, InverseFisherTransform, DecyclerOscillator, RoofingFilter, EmpiricalModeDecomposition, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, ValueArea, InitialBalance, OpeningRange, Doji, Hammer, InvertedHammer, HangingMan, ShootingStar, Engulfing, Harami, MorningEveningStar, ThreeSoldiersOrCrows, PiercingDarkCloud, Marubozu, Tweezer, SpinningTop, ThreeInside, ThreeOutside, TwoCrows, UpsideGapTwoCrows, IdenticalThreeCrows, ThreeLineStrike, ThreeStarsInSouth, AbandonedBaby, AdvanceBlock, BeltHold, Breakaway, Counterattack, DojiStar, DragonflyDoji, GravestoneDoji, LongLeggedDoji, RickshawMan, EveningDojiStar, MorningDojiStar, GapSideBySideWhite, HighWave, Hikkake, HikkakeModified, HomingPigeon, OnNeck, InNeck, Thrusting, SeparatingLines, Kicking, KickingByLength, LadderBottom, MatHold, MatchingLow, LongLine, ShortLine, RisingThreeMethods, FallingThreeMethods, UpsideGapThreeMethods, DownsideGapThreeMethods, StalledPattern, StickSandwich, Takuri, ClosingMarubozu, OpeningMarubozu, TasukiGap, UniqueThreeRiver, ConcealingBabySwallow, OrderBookImbalanceTop1, OrderBookImbalanceFull, Microprice, QuotedSpread, DepthSlope, OrderBookImbalanceTopN, SignedVolume, CumulativeVolumeDelta, TradeImbalance, EffectiveSpread, RealizedSpread, KylesLambda, Footprint, FundingRate, FundingRateMean, FundingRateZScore, FundingBasis, OpenInterestDelta, OIPriceDivergence, OIWeighted, LongShortRatio, TakerBuySellRatio, LiquidationFeatures, TermStructureBasis, CalendarSpread, SharpeRatio, SortinoRatio, CalmarRatio, OmegaRatio, MaxDrawdown, AverageDrawdown, DrawdownDuration, PainIndex, ValueAtRisk, ConditionalValueAtRisk, ProfitFactor, GainLossRatio, RecoveryFactor, KellyCriterion, TreynorRatio, InformationRatio, Alpha } = nativeBinding
|
||||
|
||||
module.exports.version = version
|
||||
module.exports.SMA = SMA
|
||||
@@ -515,6 +515,76 @@ module.exports.Tweezer = Tweezer
|
||||
module.exports.SpinningTop = SpinningTop
|
||||
module.exports.ThreeInside = ThreeInside
|
||||
module.exports.ThreeOutside = ThreeOutside
|
||||
module.exports.TwoCrows = TwoCrows
|
||||
module.exports.UpsideGapTwoCrows = UpsideGapTwoCrows
|
||||
module.exports.IdenticalThreeCrows = IdenticalThreeCrows
|
||||
module.exports.ThreeLineStrike = ThreeLineStrike
|
||||
module.exports.ThreeStarsInSouth = ThreeStarsInSouth
|
||||
module.exports.AbandonedBaby = AbandonedBaby
|
||||
module.exports.AdvanceBlock = AdvanceBlock
|
||||
module.exports.BeltHold = BeltHold
|
||||
module.exports.Breakaway = Breakaway
|
||||
module.exports.Counterattack = Counterattack
|
||||
module.exports.DojiStar = DojiStar
|
||||
module.exports.DragonflyDoji = DragonflyDoji
|
||||
module.exports.GravestoneDoji = GravestoneDoji
|
||||
module.exports.LongLeggedDoji = LongLeggedDoji
|
||||
module.exports.RickshawMan = RickshawMan
|
||||
module.exports.EveningDojiStar = EveningDojiStar
|
||||
module.exports.MorningDojiStar = MorningDojiStar
|
||||
module.exports.GapSideBySideWhite = GapSideBySideWhite
|
||||
module.exports.HighWave = HighWave
|
||||
module.exports.Hikkake = Hikkake
|
||||
module.exports.HikkakeModified = HikkakeModified
|
||||
module.exports.HomingPigeon = HomingPigeon
|
||||
module.exports.OnNeck = OnNeck
|
||||
module.exports.InNeck = InNeck
|
||||
module.exports.Thrusting = Thrusting
|
||||
module.exports.SeparatingLines = SeparatingLines
|
||||
module.exports.Kicking = Kicking
|
||||
module.exports.KickingByLength = KickingByLength
|
||||
module.exports.LadderBottom = LadderBottom
|
||||
module.exports.MatHold = MatHold
|
||||
module.exports.MatchingLow = MatchingLow
|
||||
module.exports.LongLine = LongLine
|
||||
module.exports.ShortLine = ShortLine
|
||||
module.exports.RisingThreeMethods = RisingThreeMethods
|
||||
module.exports.FallingThreeMethods = FallingThreeMethods
|
||||
module.exports.UpsideGapThreeMethods = UpsideGapThreeMethods
|
||||
module.exports.DownsideGapThreeMethods = DownsideGapThreeMethods
|
||||
module.exports.StalledPattern = StalledPattern
|
||||
module.exports.StickSandwich = StickSandwich
|
||||
module.exports.Takuri = Takuri
|
||||
module.exports.ClosingMarubozu = ClosingMarubozu
|
||||
module.exports.OpeningMarubozu = OpeningMarubozu
|
||||
module.exports.TasukiGap = TasukiGap
|
||||
module.exports.UniqueThreeRiver = UniqueThreeRiver
|
||||
module.exports.ConcealingBabySwallow = ConcealingBabySwallow
|
||||
module.exports.OrderBookImbalanceTop1 = OrderBookImbalanceTop1
|
||||
module.exports.OrderBookImbalanceFull = OrderBookImbalanceFull
|
||||
module.exports.Microprice = Microprice
|
||||
module.exports.QuotedSpread = QuotedSpread
|
||||
module.exports.DepthSlope = DepthSlope
|
||||
module.exports.OrderBookImbalanceTopN = OrderBookImbalanceTopN
|
||||
module.exports.SignedVolume = SignedVolume
|
||||
module.exports.CumulativeVolumeDelta = CumulativeVolumeDelta
|
||||
module.exports.TradeImbalance = TradeImbalance
|
||||
module.exports.EffectiveSpread = EffectiveSpread
|
||||
module.exports.RealizedSpread = RealizedSpread
|
||||
module.exports.KylesLambda = KylesLambda
|
||||
module.exports.Footprint = Footprint
|
||||
module.exports.FundingRate = FundingRate
|
||||
module.exports.FundingRateMean = FundingRateMean
|
||||
module.exports.FundingRateZScore = FundingRateZScore
|
||||
module.exports.FundingBasis = FundingBasis
|
||||
module.exports.OpenInterestDelta = OpenInterestDelta
|
||||
module.exports.OIPriceDivergence = OIPriceDivergence
|
||||
module.exports.OIWeighted = OIWeighted
|
||||
module.exports.LongShortRatio = LongShortRatio
|
||||
module.exports.TakerBuySellRatio = TakerBuySellRatio
|
||||
module.exports.LiquidationFeatures = LiquidationFeatures
|
||||
module.exports.TermStructureBasis = TermStructureBasis
|
||||
module.exports.CalendarSpread = CalendarSpread
|
||||
module.exports.SharpeRatio = SharpeRatio
|
||||
module.exports.SortinoRatio = SortinoRatio
|
||||
module.exports.CalmarRatio = CalmarRatio
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "wickra-darwin-arm64",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.4",
|
||||
"description": "Native binding for wickra (macOS Apple Silicon). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.darwin-arm64.node",
|
||||
"files": [
|
||||
"wickra.darwin-arm64.node"
|
||||
],
|
||||
"license": "PolyForm-Noncommercial-1.0.0",
|
||||
"license": "LicenseRef-Wickra-Noncommercial-1.0.0",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "wickra-darwin-x64",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.4",
|
||||
"description": "Native binding for wickra (macOS Intel). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.darwin-x64.node",
|
||||
"files": [
|
||||
"wickra.darwin-x64.node"
|
||||
],
|
||||
"license": "PolyForm-Noncommercial-1.0.0",
|
||||
"license": "LicenseRef-Wickra-Noncommercial-1.0.0",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "wickra-linux-arm64-gnu",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.4",
|
||||
"description": "Native binding for wickra (linux arm64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.linux-arm64-gnu.node",
|
||||
"files": [
|
||||
"wickra.linux-arm64-gnu.node"
|
||||
],
|
||||
"license": "PolyForm-Noncommercial-1.0.0",
|
||||
"license": "LicenseRef-Wickra-Noncommercial-1.0.0",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "wickra-linux-x64-gnu",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.4",
|
||||
"description": "Native binding for wickra (linux x64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.linux-x64-gnu.node",
|
||||
"files": [
|
||||
"wickra.linux-x64-gnu.node"
|
||||
],
|
||||
"license": "PolyForm-Noncommercial-1.0.0",
|
||||
"license": "LicenseRef-Wickra-Noncommercial-1.0.0",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "wickra-win32-arm64-msvc",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.4",
|
||||
"description": "Native binding for wickra (Windows arm64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.win32-arm64-msvc.node",
|
||||
"files": [
|
||||
"wickra.win32-arm64-msvc.node"
|
||||
],
|
||||
"license": "PolyForm-Noncommercial-1.0.0",
|
||||
"license": "LicenseRef-Wickra-Noncommercial-1.0.0",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "wickra-win32-x64-msvc",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.4",
|
||||
"description": "Native binding for wickra (Windows x64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.win32-x64-msvc.node",
|
||||
"files": [
|
||||
"wickra.win32-x64-msvc.node"
|
||||
],
|
||||
"license": "PolyForm-Noncommercial-1.0.0",
|
||||
"license": "LicenseRef-Wickra-Noncommercial-1.0.0",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
|
||||
Generated
+20
-20
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "wickra",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "wickra",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.4",
|
||||
"license": "PolyForm-Noncommercial-1.0.0",
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "^2.18.0"
|
||||
@@ -15,12 +15,12 @@
|
||||
"node": ">= 18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"wickra-darwin-arm64": "0.4.1",
|
||||
"wickra-darwin-x64": "0.4.1",
|
||||
"wickra-linux-arm64-gnu": "0.4.1",
|
||||
"wickra-linux-x64-gnu": "0.4.1",
|
||||
"wickra-win32-arm64-msvc": "0.4.1",
|
||||
"wickra-win32-x64-msvc": "0.4.1"
|
||||
"wickra-darwin-arm64": "0.4.4",
|
||||
"wickra-darwin-x64": "0.4.4",
|
||||
"wickra-linux-arm64-gnu": "0.4.4",
|
||||
"wickra-linux-x64-gnu": "0.4.4",
|
||||
"wickra-win32-arm64-msvc": "0.4.4",
|
||||
"wickra-win32-x64-msvc": "0.4.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/cli": {
|
||||
@@ -41,8 +41,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-darwin-arm64": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.4.1.tgz",
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.4.4.tgz",
|
||||
"integrity": "sha512-4eZiBR/yGUdr4nzhEUFy2i69XgNx64iI2ax/LPamsThgylC0KpHOZKK19QzJ2d9KbK4C8nMjME5FLuR+4GNEwQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
@@ -57,8 +57,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-darwin-x64": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.4.1.tgz",
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.4.4.tgz",
|
||||
"integrity": "sha512-6hf8zI3QPjTFp4zCpmgUwDvNtu6jHqNUHKD5e55POo0CgA52HkpyxSPtVm8TGTIZDI7kPjlbOdBM8CJ76mmXwA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
@@ -73,8 +73,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-linux-arm64-gnu": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.4.1.tgz",
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.4.4.tgz",
|
||||
"integrity": "sha512-kSe6y0xBMSiqdPLXNjwop5WZdHtvdBNKSEBCwZ4hFq33p4apW25/wrlzv9/oDuyD4kuPabJEhCCnFOplh58CUg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
@@ -89,8 +89,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-linux-x64-gnu": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.4.1.tgz",
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.4.4.tgz",
|
||||
"integrity": "sha512-tWBWS4qz7hxM4xnpFb59bhf6TaLwXq0Z3jEa/2l7r8PiHA94g8r8S53NRMiT+4yiL5hSWe/nUiC/YXdRrhEZ4g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
@@ -105,8 +105,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-win32-arm64-msvc": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.4.1.tgz",
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.4.4.tgz",
|
||||
"integrity": "sha512-EXIckHxAtF75PUGDKRzXyqMe9ldP0JjSdu68WFN6iJfp+McYrGu6h40TEJlQ/oUEIoPqiZB/xhVyo/el5Lg7zw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
@@ -121,8 +121,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-win32-x64-msvc": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.4.1.tgz",
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.4.4.tgz",
|
||||
"integrity": "sha512-Yfsqq1Xwp6hdxMyLze411vNdo7BDwI6+lPSe7A9XdqyPecNDbtKwYLpsal2r8EHbNzqM+R8XnuRtUaEQS5VlUQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "wickra",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.4",
|
||||
"description": "Streaming-first technical indicators: incremental, fast, install-free. Node bindings powered by Rust.",
|
||||
"author": "kingchenc <support@wickra.org>",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"license": "PolyForm-Noncommercial-1.0.0",
|
||||
"license": "LicenseRef-Wickra-Noncommercial-1.0.0",
|
||||
"keywords": [
|
||||
"trading",
|
||||
"indicators",
|
||||
@@ -47,12 +47,12 @@
|
||||
"node": ">= 18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"wickra-linux-x64-gnu": "0.4.1",
|
||||
"wickra-linux-arm64-gnu": "0.4.1",
|
||||
"wickra-darwin-x64": "0.4.1",
|
||||
"wickra-darwin-arm64": "0.4.1",
|
||||
"wickra-win32-x64-msvc": "0.4.1",
|
||||
"wickra-win32-arm64-msvc": "0.4.1"
|
||||
"wickra-linux-x64-gnu": "0.4.4",
|
||||
"wickra-linux-arm64-gnu": "0.4.4",
|
||||
"wickra-darwin-x64": "0.4.4",
|
||||
"wickra-darwin-arm64": "0.4.4",
|
||||
"wickra-win32-x64-msvc": "0.4.4",
|
||||
"wickra-win32-arm64-msvc": "0.4.4"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "napi build --platform --release",
|
||||
|
||||
+1578
-2
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@ version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
license-file.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
readme.workspace = true
|
||||
|
||||
@@ -4,10 +4,10 @@ build-backend = "maturin"
|
||||
|
||||
[project]
|
||||
name = "wickra"
|
||||
version = "0.4.1"
|
||||
version = "0.4.4"
|
||||
description = "Streaming-first technical indicators: incremental, fast, install-free."
|
||||
readme = "README.md"
|
||||
license = { text = "PolyForm-Noncommercial-1.0.0" }
|
||||
license = { text = "PolyForm-Noncommercial-1.0.0 with additional personal-account permissions; see LICENSE" }
|
||||
authors = [{ name = "kingchenc", email = "support@wickra.org" }]
|
||||
requires-python = ">=3.9"
|
||||
keywords = ["finance", "trading", "indicators", "technical-analysis", "ta-lib"]
|
||||
|
||||
@@ -240,6 +240,81 @@ from ._wickra import (
|
||||
SpinningTop,
|
||||
ThreeInside,
|
||||
ThreeOutside,
|
||||
TwoCrows,
|
||||
UpsideGapTwoCrows,
|
||||
IdenticalThreeCrows,
|
||||
ThreeLineStrike,
|
||||
ThreeStarsInSouth,
|
||||
AbandonedBaby,
|
||||
AdvanceBlock,
|
||||
BeltHold,
|
||||
Breakaway,
|
||||
Counterattack,
|
||||
DojiStar,
|
||||
DragonflyDoji,
|
||||
GravestoneDoji,
|
||||
LongLeggedDoji,
|
||||
RickshawMan,
|
||||
EveningDojiStar,
|
||||
MorningDojiStar,
|
||||
GapSideBySideWhite,
|
||||
HighWave,
|
||||
Hikkake,
|
||||
HikkakeModified,
|
||||
HomingPigeon,
|
||||
OnNeck,
|
||||
InNeck,
|
||||
Thrusting,
|
||||
SeparatingLines,
|
||||
Kicking,
|
||||
KickingByLength,
|
||||
LadderBottom,
|
||||
MatHold,
|
||||
MatchingLow,
|
||||
LongLine,
|
||||
ShortLine,
|
||||
RisingThreeMethods,
|
||||
FallingThreeMethods,
|
||||
UpsideGapThreeMethods,
|
||||
DownsideGapThreeMethods,
|
||||
StalledPattern,
|
||||
StickSandwich,
|
||||
Takuri,
|
||||
ClosingMarubozu,
|
||||
OpeningMarubozu,
|
||||
TasukiGap,
|
||||
UniqueThreeRiver,
|
||||
ConcealingBabySwallow,
|
||||
# Microstructure: order book
|
||||
OrderBookImbalanceTop1,
|
||||
OrderBookImbalanceTopN,
|
||||
OrderBookImbalanceFull,
|
||||
Microprice,
|
||||
QuotedSpread,
|
||||
DepthSlope,
|
||||
# Microstructure: trade flow
|
||||
SignedVolume,
|
||||
CumulativeVolumeDelta,
|
||||
TradeImbalance,
|
||||
# Microstructure: price impact
|
||||
EffectiveSpread,
|
||||
RealizedSpread,
|
||||
KylesLambda,
|
||||
# Microstructure: footprint
|
||||
Footprint,
|
||||
# Derivatives
|
||||
FundingRate,
|
||||
FundingRateMean,
|
||||
FundingRateZScore,
|
||||
FundingBasis,
|
||||
OpenInterestDelta,
|
||||
OIPriceDivergence,
|
||||
OIWeighted,
|
||||
LongShortRatio,
|
||||
TakerBuySellRatio,
|
||||
LiquidationFeatures,
|
||||
TermStructureBasis,
|
||||
CalendarSpread,
|
||||
# Risk / Performance
|
||||
SharpeRatio,
|
||||
SortinoRatio,
|
||||
@@ -477,6 +552,81 @@ __all__ = [
|
||||
"SpinningTop",
|
||||
"ThreeInside",
|
||||
"ThreeOutside",
|
||||
"TwoCrows",
|
||||
"UpsideGapTwoCrows",
|
||||
"IdenticalThreeCrows",
|
||||
"ThreeLineStrike",
|
||||
"ThreeStarsInSouth",
|
||||
"AbandonedBaby",
|
||||
"AdvanceBlock",
|
||||
"BeltHold",
|
||||
"Breakaway",
|
||||
"Counterattack",
|
||||
"DojiStar",
|
||||
"DragonflyDoji",
|
||||
"GravestoneDoji",
|
||||
"LongLeggedDoji",
|
||||
"RickshawMan",
|
||||
"EveningDojiStar",
|
||||
"MorningDojiStar",
|
||||
"GapSideBySideWhite",
|
||||
"HighWave",
|
||||
"Hikkake",
|
||||
"HikkakeModified",
|
||||
"HomingPigeon",
|
||||
"OnNeck",
|
||||
"InNeck",
|
||||
"Thrusting",
|
||||
"SeparatingLines",
|
||||
"Kicking",
|
||||
"KickingByLength",
|
||||
"LadderBottom",
|
||||
"MatHold",
|
||||
"MatchingLow",
|
||||
"LongLine",
|
||||
"ShortLine",
|
||||
"RisingThreeMethods",
|
||||
"FallingThreeMethods",
|
||||
"UpsideGapThreeMethods",
|
||||
"DownsideGapThreeMethods",
|
||||
"StalledPattern",
|
||||
"StickSandwich",
|
||||
"Takuri",
|
||||
"ClosingMarubozu",
|
||||
"OpeningMarubozu",
|
||||
"TasukiGap",
|
||||
"UniqueThreeRiver",
|
||||
"ConcealingBabySwallow",
|
||||
# Microstructure: order book
|
||||
"OrderBookImbalanceTop1",
|
||||
"OrderBookImbalanceTopN",
|
||||
"OrderBookImbalanceFull",
|
||||
"Microprice",
|
||||
"QuotedSpread",
|
||||
"DepthSlope",
|
||||
# Microstructure: trade flow
|
||||
"SignedVolume",
|
||||
"CumulativeVolumeDelta",
|
||||
"TradeImbalance",
|
||||
# Microstructure: price impact
|
||||
"EffectiveSpread",
|
||||
"RealizedSpread",
|
||||
"KylesLambda",
|
||||
# Microstructure: footprint
|
||||
"Footprint",
|
||||
# Derivatives
|
||||
"FundingRate",
|
||||
"FundingRateMean",
|
||||
"FundingRateZScore",
|
||||
"FundingBasis",
|
||||
"OpenInterestDelta",
|
||||
"OIPriceDivergence",
|
||||
"OIWeighted",
|
||||
"LongShortRatio",
|
||||
"TakerBuySellRatio",
|
||||
"LiquidationFeatures",
|
||||
"TermStructureBasis",
|
||||
"CalendarSpread",
|
||||
# Risk / Performance
|
||||
"SharpeRatio",
|
||||
"SortinoRatio",
|
||||
|
||||
+1634
-4
File diff suppressed because it is too large
Load Diff
@@ -166,3 +166,115 @@ def test_family_10_ehlers_rejects_invalid_parameters():
|
||||
ta.MAMA(0.05, 0.5)
|
||||
with pytest.raises(ValueError):
|
||||
ta.EmpiricalModeDecomposition(20, 0.0)
|
||||
|
||||
|
||||
def test_orderbook_topn_zero_levels_raises():
|
||||
with pytest.raises(ValueError):
|
||||
ta.OrderBookImbalanceTopN(0)
|
||||
|
||||
|
||||
def test_orderbook_unequal_price_size_lengths_raise():
|
||||
# bid_px has 2 entries but bid_sz has 1 -> mismatched -> ValueError.
|
||||
with pytest.raises(ValueError):
|
||||
ta.OrderBookImbalanceTop1().update([100.0, 99.0], [1.0], [101.0], [1.0])
|
||||
with pytest.raises(ValueError):
|
||||
ta.Microprice().update([100.0], [1.0], [101.0, 102.0], [1.0])
|
||||
|
||||
|
||||
def test_orderbook_crossed_book_raises():
|
||||
# best_bid (102) >= best_ask (101) is a crossed book -> rejected.
|
||||
with pytest.raises(ValueError):
|
||||
ta.QuotedSpread().update([102.0], [1.0], [101.0], [1.0])
|
||||
|
||||
|
||||
def test_orderbook_misordered_levels_raise():
|
||||
# Bids must be strictly descending in price.
|
||||
with pytest.raises(ValueError):
|
||||
ta.OrderBookImbalanceFull().update([99.0, 100.0], [1.0, 1.0], [101.0], [1.0])
|
||||
|
||||
|
||||
def test_trade_imbalance_zero_window_raises():
|
||||
with pytest.raises(ValueError):
|
||||
ta.TradeImbalance(0)
|
||||
|
||||
|
||||
def test_trade_negative_size_raises():
|
||||
with pytest.raises(ValueError):
|
||||
ta.SignedVolume().update(100.0, -1.0, True)
|
||||
|
||||
|
||||
def test_trade_non_positive_price_raises():
|
||||
with pytest.raises(ValueError):
|
||||
ta.CumulativeVolumeDelta().update(0.0, 1.0, True)
|
||||
|
||||
|
||||
def test_trade_batch_unequal_lengths_raise():
|
||||
with pytest.raises(ValueError):
|
||||
ta.SignedVolume().batch([100.0, 100.0], [1.0], [True, False])
|
||||
|
||||
|
||||
def test_effective_spread_non_positive_mid_raises():
|
||||
with pytest.raises(ValueError):
|
||||
ta.EffectiveSpread().update(100.0, 1.0, True, 0.0)
|
||||
|
||||
|
||||
def test_effective_spread_batch_unequal_lengths_raise():
|
||||
with pytest.raises(ValueError):
|
||||
ta.EffectiveSpread().batch([100.0, 100.0], [1.0, 1.0], [True, False], [100.0])
|
||||
|
||||
|
||||
def test_realized_spread_zero_horizon_raises():
|
||||
with pytest.raises(ValueError):
|
||||
ta.RealizedSpread(0)
|
||||
|
||||
|
||||
def test_kyles_lambda_window_below_two_raises():
|
||||
with pytest.raises(ValueError):
|
||||
ta.KylesLambda(1)
|
||||
|
||||
|
||||
def test_footprint_non_positive_tick_raises():
|
||||
with pytest.raises(ValueError):
|
||||
ta.Footprint(0.0)
|
||||
with pytest.raises(ValueError):
|
||||
ta.Footprint(-1.0)
|
||||
|
||||
|
||||
def test_funding_rate_mean_zero_window_raises():
|
||||
with pytest.raises(ValueError):
|
||||
ta.FundingRateMean(0)
|
||||
|
||||
|
||||
def test_funding_rate_zscore_zero_window_raises():
|
||||
with pytest.raises(ValueError):
|
||||
ta.FundingRateZScore(0)
|
||||
|
||||
|
||||
def test_funding_basis_non_positive_index_raises():
|
||||
with pytest.raises(ValueError):
|
||||
ta.FundingBasis().update(100.0, 0.0)
|
||||
|
||||
|
||||
def test_funding_rate_non_finite_raises():
|
||||
with pytest.raises(ValueError):
|
||||
ta.FundingRate().update(float("nan"))
|
||||
|
||||
|
||||
def test_oi_price_divergence_zero_window_raises():
|
||||
with pytest.raises(ValueError):
|
||||
ta.OIPriceDivergence(0)
|
||||
|
||||
|
||||
def test_oi_weighted_non_positive_mark_raises():
|
||||
with pytest.raises(ValueError):
|
||||
ta.OIWeighted().update(0.0, 100.0)
|
||||
|
||||
|
||||
def test_term_structure_basis_non_positive_index_raises():
|
||||
with pytest.raises(ValueError):
|
||||
ta.TermStructureBasis().update(100.0, 0.0)
|
||||
|
||||
|
||||
def test_calendar_spread_non_positive_mark_raises():
|
||||
with pytest.raises(ValueError):
|
||||
ta.CalendarSpread().update(100.0, 0.0)
|
||||
|
||||
@@ -823,3 +823,206 @@ def test_yang_zhang_zero_movement_yields_zero():
|
||||
ready = out[~np.isnan(out)]
|
||||
assert ready.size > 0
|
||||
np.testing.assert_allclose(ready, 0.0, atol=1e-12)
|
||||
|
||||
|
||||
def test_doji_default_is_directionless_flag():
|
||||
# Default Doji is a direction-less detection flag: +1 on a doji, 0 else.
|
||||
d = ta.Doji()
|
||||
assert d.is_signed() is False
|
||||
# body 0, range 2 -> doji.
|
||||
assert d.update((10.0, 11.0, 9.0, 10.0, 1.0, 0)) == pytest.approx(1.0)
|
||||
# body 2 == range -> not a doji.
|
||||
assert d.update((10.0, 12.0, 10.0, 12.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
|
||||
|
||||
def test_doji_signed_dragonfly_gravestone_neutral():
|
||||
# Signed Doji classifies by body position within the range.
|
||||
d = ta.Doji(signed=True)
|
||||
assert d.is_signed() is True
|
||||
# Dragonfly: body at the top, long lower shadow -> bullish +1.
|
||||
assert d.update((10.0, 10.05, 6.0, 10.0, 1.0, 0)) == pytest.approx(1.0)
|
||||
# Gravestone: body at the bottom, long upper shadow -> bearish -1.
|
||||
assert d.update((10.0, 14.0, 9.95, 10.0, 1.0, 1)) == pytest.approx(-1.0)
|
||||
# Long-legged: body centred, symmetric shadows -> neutral 0.
|
||||
assert d.update((10.0, 12.0, 8.0, 10.0, 1.0, 2)) == pytest.approx(0.0)
|
||||
# A large body is not a doji at all -> 0 regardless of position.
|
||||
assert d.update((10.0, 12.0, 10.0, 12.0, 1.0, 3)) == pytest.approx(0.0)
|
||||
|
||||
|
||||
def test_orderbook_imbalance_reference_values():
|
||||
# Top-1: (3 - 1) / (3 + 1) = 0.5.
|
||||
assert ta.OrderBookImbalanceTop1().update([100.0], [3.0], [101.0], [1.0]) == pytest.approx(0.5)
|
||||
# Top-2: bidDepth 3, askDepth 2 -> (3 - 2) / 5 = 0.2.
|
||||
topn = ta.OrderBookImbalanceTopN(2)
|
||||
assert topn.update([100.0, 99.0], [2.0, 1.0], [101.0, 102.0], [1.0, 1.0]) == pytest.approx(0.2)
|
||||
# Full: bidDepth 1, askDepth 3 -> (1 - 3) / 4 = -0.5.
|
||||
full = ta.OrderBookImbalanceFull()
|
||||
assert full.update([100.0], [1.0], [101.0, 102.0], [2.0, 1.0]) == pytest.approx(-0.5)
|
||||
|
||||
|
||||
def test_microprice_reference_value():
|
||||
# (100*3 + 101*1) / (1 + 3) = 401 / 4 = 100.25 — heavy ask pulls toward bid.
|
||||
mp = ta.Microprice()
|
||||
assert mp.update([100.0], [1.0], [101.0], [3.0]) == pytest.approx(100.25)
|
||||
|
||||
|
||||
def test_quoted_spread_reference_value():
|
||||
# spread 1.0, mid 100.5 -> 1 / 100.5 * 10_000 ≈ 99.5025 bps.
|
||||
qs = ta.QuotedSpread()
|
||||
assert qs.update([100.0], [1.0], [101.0], [1.0]) == pytest.approx(99.50248756, abs=1e-6)
|
||||
|
||||
|
||||
def test_depth_slope_reference_value():
|
||||
# Symmetric book, each side distances 1, 2 with cumulative sizes 1, 3.
|
||||
# OLS slope of (1->1, 2->3) = 2; mean of two equal sides = 2.
|
||||
ds = ta.DepthSlope()
|
||||
out = ds.update([99.0, 98.0], [1.0, 2.0], [101.0, 102.0], [1.0, 2.0])
|
||||
assert out == pytest.approx(2.0, abs=1e-9)
|
||||
# A book with a single level per side has no slope -> 0.
|
||||
assert ta.DepthSlope().update([100.0], [1.0], [101.0], [1.0]) == pytest.approx(0.0)
|
||||
|
||||
|
||||
def test_footprint_buckets_buy_and_sell_volume():
|
||||
fp = ta.Footprint(1.0)
|
||||
fp.update(100.2, 2.0, True) # bucket 100 -> ask 2
|
||||
fp.update(100.7, 3.0, False) # bucket 101 -> bid 3
|
||||
out = fp.update(100.1, 1.0, True) # bucket 100 -> ask 3
|
||||
# Columns are [price, bid_vol, ask_vol], rows sorted ascending by price.
|
||||
assert out.shape == (2, 3)
|
||||
assert list(out[0]) == [100.0, 0.0, 3.0]
|
||||
assert list(out[1]) == [101.0, 3.0, 0.0]
|
||||
|
||||
|
||||
def test_signed_volume_reference_values():
|
||||
assert ta.SignedVolume().update(100.0, 2.0, True) == pytest.approx(2.0)
|
||||
assert ta.SignedVolume().update(100.0, 3.0, False) == pytest.approx(-3.0)
|
||||
|
||||
|
||||
def test_cumulative_volume_delta_reference_values():
|
||||
cvd = ta.CumulativeVolumeDelta()
|
||||
assert cvd.update(100.0, 5.0, True) == pytest.approx(5.0)
|
||||
assert cvd.update(100.0, 2.0, False) == pytest.approx(3.0)
|
||||
assert cvd.update(100.0, 4.0, False) == pytest.approx(-1.0)
|
||||
|
||||
|
||||
def test_trade_imbalance_reference_value():
|
||||
ti = ta.TradeImbalance(2)
|
||||
assert ti.update(100.0, 3.0, True) is None # warming up
|
||||
# Window full: buyVol 3, sellVol 1 -> (3 - 1) / 4 = 0.5.
|
||||
assert ti.update(100.0, 1.0, False) == pytest.approx(0.5)
|
||||
|
||||
|
||||
def test_effective_spread_reference_values():
|
||||
# Buy at 100.05 vs mid 100.0: 2 * (100.05 - 100) / 100 * 10000 = 10 bps.
|
||||
assert ta.EffectiveSpread().update(100.05, 1.0, True, 100.0) == pytest.approx(10.0)
|
||||
# Sell at 99.95 vs mid 100.0: 2 * -1 * (99.95 - 100) / 100 * 10000 = 10 bps.
|
||||
assert ta.EffectiveSpread().update(99.95, 1.0, False, 100.0) == pytest.approx(10.0)
|
||||
# A buy filled below the mid is price improvement -> negative.
|
||||
assert ta.EffectiveSpread().update(99.95, 1.0, True, 100.0) < 0.0
|
||||
|
||||
|
||||
def test_realized_spread_reference_value():
|
||||
rs = ta.RealizedSpread(1)
|
||||
assert rs.update(100.10, 1.0, True, 100.0) is None # buffered
|
||||
# Resolved against mid 100.20 one trade later:
|
||||
# 2 * (+1) * (100.10 - 100.20) / 100.0 * 10000 = -20 bps (adverse selection).
|
||||
assert rs.update(99.90, 1.0, False, 100.20) == pytest.approx(-20.0)
|
||||
|
||||
|
||||
def test_kyles_lambda_recovers_constant_impact():
|
||||
# Build a tape where each trade moves the mid by exactly 0.5 per unit of
|
||||
# signed volume -> the rolling OLS slope is 0.5.
|
||||
impact = 0.5
|
||||
mid = 100.0
|
||||
price, size, is_buy, mids = [], [], [], []
|
||||
for i in range(20):
|
||||
buy = i % 2 == 0
|
||||
sz = 1.0 + (i % 3)
|
||||
signed = sz if buy else -sz
|
||||
mid += impact * signed
|
||||
price.append(mid)
|
||||
size.append(sz)
|
||||
is_buy.append(buy)
|
||||
mids.append(mid)
|
||||
out = ta.KylesLambda(6).batch(price, size, is_buy, mids)
|
||||
assert out[-1] == pytest.approx(0.5, abs=1e-9)
|
||||
|
||||
|
||||
def test_funding_rate_reference_values():
|
||||
assert ta.FundingRate().update(0.0001) == pytest.approx(0.0001)
|
||||
assert ta.FundingRate().update(-0.0003) == pytest.approx(-0.0003)
|
||||
|
||||
|
||||
def test_funding_rate_mean_reference_value():
|
||||
frm = ta.FundingRateMean(2)
|
||||
assert frm.update(0.001) is None # warming up
|
||||
# Window [0.001, 0.003] -> mean 0.002.
|
||||
assert frm.update(0.003) == pytest.approx(0.002)
|
||||
|
||||
|
||||
def test_funding_rate_zscore_reference_value():
|
||||
z = ta.FundingRateZScore(2)
|
||||
assert z.update(0.001) is None # warming up
|
||||
# Window [0.001, 0.003]: mean 0.002, population stddev 0.001 -> +1.
|
||||
assert z.update(0.003) == pytest.approx(1.0, abs=1e-9)
|
||||
|
||||
|
||||
def test_funding_basis_reference_value():
|
||||
# mark 100.5 vs index 100.0 -> (100.5 - 100.0) / 100.0 = 0.005.
|
||||
assert ta.FundingBasis().update(100.5, 100.0) == pytest.approx(0.005)
|
||||
# A discount reads negative.
|
||||
assert ta.FundingBasis().update(99.5, 100.0) == pytest.approx(-0.005)
|
||||
|
||||
|
||||
def test_open_interest_delta_reference_value():
|
||||
oid = ta.OpenInterestDelta()
|
||||
assert oid.update(1000.0) is None # seeds the previous OI
|
||||
assert oid.update(1250.0) == pytest.approx(250.0)
|
||||
assert oid.update(1100.0) == pytest.approx(-150.0)
|
||||
|
||||
|
||||
def test_oi_price_divergence_reference_value():
|
||||
div = ta.OIPriceDivergence(1)
|
||||
assert div.update(1000.0, 100.0) is None # warming up
|
||||
# OI +10% while price flat -> divergence +0.1.
|
||||
assert div.update(1100.0, 100.0) == pytest.approx(0.1)
|
||||
|
||||
|
||||
def test_oi_weighted_reference_value():
|
||||
oiw = ta.OIWeighted()
|
||||
assert oiw.update(100.0, 10.0) == pytest.approx(100.0)
|
||||
# (100·10 + 110·30) / 40 = 107.5.
|
||||
assert oiw.update(110.0, 30.0) == pytest.approx(107.5)
|
||||
|
||||
|
||||
def test_long_short_ratio_reference_value():
|
||||
# 600 longs vs 400 shorts -> 1.5.
|
||||
assert ta.LongShortRatio().update(600.0, 400.0) == pytest.approx(1.5)
|
||||
# No short side -> 0.0.
|
||||
assert ta.LongShortRatio().update(600.0, 0.0) == pytest.approx(0.0)
|
||||
|
||||
|
||||
def test_taker_buy_sell_ratio_reference_value():
|
||||
# 60 taker buys vs 40 taker sells -> 1.5.
|
||||
assert ta.TakerBuySellRatio().update(60.0, 40.0) == pytest.approx(1.5)
|
||||
# No taker sell volume -> 0.0.
|
||||
assert ta.TakerBuySellRatio().update(60.0, 0.0) == pytest.approx(0.0)
|
||||
|
||||
|
||||
def test_liquidation_features_reference_value():
|
||||
# 30 long vs 10 short: (long, short, net, total, imbalance).
|
||||
out = ta.LiquidationFeatures().update(30.0, 10.0)
|
||||
assert out == pytest.approx((30.0, 10.0, 20.0, 40.0, 0.5))
|
||||
|
||||
|
||||
def test_term_structure_basis_reference_value():
|
||||
# futures 102 vs index 100 -> 0.02 (contango).
|
||||
assert ta.TermStructureBasis().update(102.0, 100.0) == pytest.approx(0.02)
|
||||
# Backwardation reads negative.
|
||||
assert ta.TermStructureBasis().update(98.0, 100.0) == pytest.approx(-0.02)
|
||||
|
||||
|
||||
def test_calendar_spread_reference_value():
|
||||
# futures 101 vs perpetual mark 100 -> 0.01.
|
||||
assert ta.CalendarSpread().update(101.0, 100.0) == pytest.approx(0.01)
|
||||
assert ta.CalendarSpread().update(99.0, 100.0) == pytest.approx(-0.01)
|
||||
|
||||
@@ -129,3 +129,92 @@ def test_ehlers_indicators_lifecycle():
|
||||
assert ind.is_ready()
|
||||
ind.reset()
|
||||
assert not ind.is_ready()
|
||||
|
||||
|
||||
def test_orderbook_lifecycle():
|
||||
snapshot = ([100.0], [1.0], [101.0], [1.0])
|
||||
for ind in [
|
||||
ta.OrderBookImbalanceTop1(),
|
||||
ta.OrderBookImbalanceTopN(3),
|
||||
ta.OrderBookImbalanceFull(),
|
||||
ta.Microprice(),
|
||||
ta.QuotedSpread(),
|
||||
ta.DepthSlope(),
|
||||
]:
|
||||
assert ind.warmup_period() == 1
|
||||
assert not ind.is_ready()
|
||||
ind.update(*snapshot)
|
||||
assert ind.is_ready()
|
||||
ind.reset()
|
||||
assert not ind.is_ready()
|
||||
|
||||
|
||||
def test_orderbook_topn_repr():
|
||||
assert repr(ta.OrderBookImbalanceTopN(5)) == "OrderBookImbalanceTopN(levels=5)"
|
||||
|
||||
|
||||
def test_tradeflow_lifecycle():
|
||||
for ind in [ta.SignedVolume(), ta.CumulativeVolumeDelta()]:
|
||||
assert ind.warmup_period() == 1
|
||||
assert not ind.is_ready()
|
||||
ind.update(100.0, 1.0, True)
|
||||
assert ind.is_ready()
|
||||
ind.reset()
|
||||
assert not ind.is_ready()
|
||||
|
||||
|
||||
def test_trade_imbalance_lifecycle_and_repr():
|
||||
ti = ta.TradeImbalance(3)
|
||||
assert ti.warmup_period() == 3
|
||||
assert not ti.is_ready()
|
||||
for _ in range(3):
|
||||
ti.update(100.0, 1.0, True)
|
||||
assert ti.is_ready()
|
||||
ti.reset()
|
||||
assert not ti.is_ready()
|
||||
assert repr(ta.TradeImbalance(4)) == "TradeImbalance(window=4)"
|
||||
|
||||
|
||||
def test_effective_spread_lifecycle():
|
||||
es = ta.EffectiveSpread()
|
||||
assert es.warmup_period() == 1
|
||||
assert not es.is_ready()
|
||||
es.update(100.05, 1.0, True, 100.0)
|
||||
assert es.is_ready()
|
||||
es.reset()
|
||||
assert not es.is_ready()
|
||||
|
||||
|
||||
def test_realized_spread_lifecycle_and_repr():
|
||||
rs = ta.RealizedSpread(3)
|
||||
assert rs.warmup_period() == 4
|
||||
assert not rs.is_ready()
|
||||
for _ in range(4):
|
||||
rs.update(100.0, 1.0, True, 100.0)
|
||||
assert rs.is_ready()
|
||||
rs.reset()
|
||||
assert not rs.is_ready()
|
||||
assert repr(ta.RealizedSpread(5)) == "RealizedSpread(horizon=5)"
|
||||
|
||||
|
||||
def test_kyles_lambda_lifecycle_and_repr():
|
||||
kl = ta.KylesLambda(3)
|
||||
assert kl.warmup_period() == 4
|
||||
assert not kl.is_ready()
|
||||
for i in range(4):
|
||||
kl.update(100.0 + i, 1.0 + (i % 2), i % 2 == 0, 100.0 + i)
|
||||
assert kl.is_ready()
|
||||
kl.reset()
|
||||
assert not kl.is_ready()
|
||||
assert repr(ta.KylesLambda(7)) == "KylesLambda(window=7)"
|
||||
|
||||
|
||||
def test_footprint_lifecycle_and_repr():
|
||||
fp = ta.Footprint(0.5)
|
||||
assert fp.warmup_period() == 1
|
||||
assert not fp.is_ready()
|
||||
fp.update(100.0, 1.0, True)
|
||||
assert fp.is_ready()
|
||||
fp.reset()
|
||||
assert not fp.is_ready()
|
||||
assert repr(ta.Footprint(0.25)) == "Footprint(tick_size=0.25)"
|
||||
|
||||
@@ -523,6 +523,186 @@ CANDLE_SCALAR = {
|
||||
lambda: ta.ThreeOutside(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"TwoCrows": (
|
||||
lambda: ta.TwoCrows(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"UpsideGapTwoCrows": (
|
||||
lambda: ta.UpsideGapTwoCrows(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"IdenticalThreeCrows": (
|
||||
lambda: ta.IdenticalThreeCrows(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"ThreeLineStrike": (
|
||||
lambda: ta.ThreeLineStrike(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"ThreeStarsInSouth": (
|
||||
lambda: ta.ThreeStarsInSouth(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"AbandonedBaby": (
|
||||
lambda: ta.AbandonedBaby(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"AdvanceBlock": (
|
||||
lambda: ta.AdvanceBlock(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"BeltHold": (
|
||||
lambda: ta.BeltHold(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"Breakaway": (
|
||||
lambda: ta.Breakaway(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"Counterattack": (
|
||||
lambda: ta.Counterattack(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"DojiStar": (
|
||||
lambda: ta.DojiStar(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"DragonflyDoji": (
|
||||
lambda: ta.DragonflyDoji(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"GravestoneDoji": (
|
||||
lambda: ta.GravestoneDoji(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"LongLeggedDoji": (
|
||||
lambda: ta.LongLeggedDoji(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"RickshawMan": (
|
||||
lambda: ta.RickshawMan(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"EveningDojiStar": (
|
||||
lambda: ta.EveningDojiStar(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"MorningDojiStar": (
|
||||
lambda: ta.MorningDojiStar(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"GapSideBySideWhite": (
|
||||
lambda: ta.GapSideBySideWhite(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"HighWave": (
|
||||
lambda: ta.HighWave(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"Hikkake": (
|
||||
lambda: ta.Hikkake(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"HikkakeModified": (
|
||||
lambda: ta.HikkakeModified(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"HomingPigeon": (
|
||||
lambda: ta.HomingPigeon(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"OnNeck": (
|
||||
lambda: ta.OnNeck(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"InNeck": (
|
||||
lambda: ta.InNeck(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"Thrusting": (
|
||||
lambda: ta.Thrusting(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"SeparatingLines": (
|
||||
lambda: ta.SeparatingLines(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"Kicking": (
|
||||
lambda: ta.Kicking(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"KickingByLength": (
|
||||
lambda: ta.KickingByLength(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"LadderBottom": (
|
||||
lambda: ta.LadderBottom(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"MatHold": (
|
||||
lambda: ta.MatHold(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"MatchingLow": (
|
||||
lambda: ta.MatchingLow(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"LongLine": (
|
||||
lambda: ta.LongLine(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"ShortLine": (
|
||||
lambda: ta.ShortLine(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"RisingThreeMethods": (
|
||||
lambda: ta.RisingThreeMethods(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"FallingThreeMethods": (
|
||||
lambda: ta.FallingThreeMethods(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"UpsideGapThreeMethods": (
|
||||
lambda: ta.UpsideGapThreeMethods(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"DownsideGapThreeMethods": (
|
||||
lambda: ta.DownsideGapThreeMethods(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"StalledPattern": (
|
||||
lambda: ta.StalledPattern(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"StickSandwich": (
|
||||
lambda: ta.StickSandwich(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"Takuri": (
|
||||
lambda: ta.Takuri(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"ClosingMarubozu": (
|
||||
lambda: ta.ClosingMarubozu(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"OpeningMarubozu": (
|
||||
lambda: ta.OpeningMarubozu(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"TasukiGap": (
|
||||
lambda: ta.TasukiGap(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"UniqueThreeRiver": (
|
||||
lambda: ta.UniqueThreeRiver(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"ConcealingBabySwallow": (
|
||||
lambda: ta.ConcealingBabySwallow(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -1624,6 +1804,258 @@ def test_fractal_chaos_bands_detects_peak_and_trough():
|
||||
assert out[5, 1] == pytest.approx(0.5)
|
||||
|
||||
|
||||
|
||||
def test_belt_hold_reference():
|
||||
t = ta.BeltHold()
|
||||
assert t.update((10.0, 12.0, 10.0, 11.5, 1.0, 0)) == pytest.approx(1.0)
|
||||
assert t.update((12.0, 12.0, 10.0, 10.5, 1.0, 1)) == pytest.approx(-1.0)
|
||||
|
||||
|
||||
def test_breakaway_reference():
|
||||
t = ta.Breakaway()
|
||||
assert t.update((20.0, 20.2, 14.8, 15.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((14.0, 14.1, 11.9, 12.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((12.5, 13.0, 10.5, 11.0, 1.0, 2)) == pytest.approx(0.0)
|
||||
assert t.update((11.0, 11.5, 9.0, 9.5, 1.0, 3)) == pytest.approx(0.0)
|
||||
assert t.update((9.5, 14.7, 9.4, 14.5, 1.0, 4)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_counterattack_reference():
|
||||
t = ta.Counterattack()
|
||||
assert t.update((20.0, 20.1, 14.9, 15.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((10.0, 15.1, 9.9, 15.0, 1.0, 1)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_doji_star_reference():
|
||||
t = ta.DojiStar()
|
||||
assert t.update((20.0, 20.2, 14.8, 15.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((13.0, 13.1, 12.9, 13.0, 1.0, 1)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_dragonfly_doji_reference():
|
||||
t = ta.DragonflyDoji()
|
||||
assert t.update((10.0, 10.05, 6.0, 10.0, 1.0, 0)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_gravestone_doji_reference():
|
||||
t = ta.GravestoneDoji()
|
||||
assert t.update((10.0, 14.0, 9.95, 10.0, 1.0, 0)) == pytest.approx(-1.0)
|
||||
|
||||
|
||||
def test_long_legged_doji_reference():
|
||||
t = ta.LongLeggedDoji()
|
||||
assert t.update((10.0, 12.0, 8.0, 10.05, 1.0, 0)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_rickshaw_man_reference():
|
||||
t = ta.RickshawMan()
|
||||
assert t.update((10.0, 12.0, 8.0, 10.0, 1.0, 0)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_evening_doji_star_reference():
|
||||
t = ta.EveningDojiStar()
|
||||
assert t.update((10.0, 15.1, 9.9, 15.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((17.0, 17.1, 16.9, 17.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((16.0, 16.1, 11.9, 12.0, 1.0, 2)) == pytest.approx(-1.0)
|
||||
|
||||
|
||||
def test_morning_doji_star_reference():
|
||||
t = ta.MorningDojiStar()
|
||||
assert t.update((15.0, 15.1, 9.9, 10.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((8.0, 8.1, 7.9, 8.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((9.0, 13.1, 8.9, 13.0, 1.0, 2)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_gap_side_by_side_white_reference():
|
||||
t = ta.GapSideBySideWhite()
|
||||
assert t.update((10.0, 11.1, 9.9, 11.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((13.0, 14.1, 12.9, 14.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((13.0, 14.1, 12.9, 14.0, 1.0, 2)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_high_wave_reference():
|
||||
t = ta.HighWave()
|
||||
assert t.update((10.0, 12.0, 8.0, 10.3, 1.0, 0)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_hikkake_reference():
|
||||
t = ta.Hikkake()
|
||||
assert t.update((10.0, 15.0, 5.0, 12.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((11.0, 13.0, 8.0, 12.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((9.0, 12.0, 6.0, 7.0, 1.0, 2)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_hikkake_modified_reference():
|
||||
t = ta.HikkakeModified()
|
||||
assert t.update((10.0, 15.0, 5.0, 12.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((11.0, 13.0, 8.0, 12.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((9.0, 12.0, 6.0, 9.0, 1.0, 2)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_homing_pigeon_reference():
|
||||
t = ta.HomingPigeon()
|
||||
assert t.update((15.0, 15.1, 9.9, 10.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((14.0, 14.1, 10.9, 11.0, 1.0, 1)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_on_neck_reference():
|
||||
t = ta.OnNeck()
|
||||
assert t.update((15.0, 15.1, 9.0, 10.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((7.0, 9.1, 6.9, 9.0, 1.0, 1)) == pytest.approx(-1.0)
|
||||
|
||||
|
||||
def test_in_neck_reference():
|
||||
t = ta.InNeck()
|
||||
assert t.update((15.0, 15.1, 9.0, 10.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((7.0, 10.3, 6.9, 10.2, 1.0, 1)) == pytest.approx(-1.0)
|
||||
|
||||
|
||||
def test_thrusting_reference():
|
||||
t = ta.Thrusting()
|
||||
assert t.update((15.0, 15.1, 9.0, 10.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((7.0, 11.6, 6.9, 11.5, 1.0, 1)) == pytest.approx(-1.0)
|
||||
|
||||
|
||||
def test_separating_lines_reference():
|
||||
t = ta.SeparatingLines()
|
||||
assert t.update((12.0, 12.1, 9.9, 10.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((12.0, 14.1, 12.0, 14.0, 1.0, 1)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_kicking_reference():
|
||||
t = ta.Kicking()
|
||||
assert t.update((12.0, 12.0, 10.0, 10.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((14.0, 16.0, 14.0, 16.0, 1.0, 1)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_kicking_by_length_reference():
|
||||
t = ta.KickingByLength()
|
||||
assert t.update((12.0, 12.0, 10.0, 10.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((14.0, 20.0, 14.0, 20.0, 1.0, 1)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_ladder_bottom_reference():
|
||||
t = ta.LadderBottom()
|
||||
assert t.update((20.0, 20.1, 17.9, 18.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((18.0, 18.1, 15.9, 16.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((16.0, 16.1, 13.9, 14.0, 1.0, 2)) == pytest.approx(0.0)
|
||||
assert t.update((14.0, 15.0, 12.4, 12.5, 1.0, 3)) == pytest.approx(0.0)
|
||||
assert t.update((15.0, 17.1, 14.9, 17.0, 1.0, 4)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_mat_hold_reference():
|
||||
t = ta.MatHold()
|
||||
assert t.update((10.0, 15.1, 9.9, 15.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((16.0, 16.1, 15.4, 15.5, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((15.5, 15.6, 14.9, 15.0, 1.0, 2)) == pytest.approx(0.0)
|
||||
assert t.update((15.0, 15.1, 14.4, 14.5, 1.0, 3)) == pytest.approx(0.0)
|
||||
assert t.update((14.5, 17.1, 14.4, 17.0, 1.0, 4)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_matching_low_reference():
|
||||
t = ta.MatchingLow()
|
||||
assert t.update((15.0, 15.1, 9.9, 10.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((13.0, 13.1, 9.9, 10.0, 1.0, 1)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_long_line_reference():
|
||||
t = ta.LongLine()
|
||||
# Five quiet bars fill the rolling range average, then a wide solid white bar.
|
||||
for ts in range(5):
|
||||
assert t.update((10.0, 10.5, 9.5, 10.2, 1.0, ts)) == pytest.approx(0.0)
|
||||
assert t.update((10.0, 13.0, 9.9, 12.9, 1.0, 5)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_short_line_reference():
|
||||
t = ta.ShortLine()
|
||||
# Five wide bars fill the rolling range average, then a compact solid white bar.
|
||||
for ts in range(5):
|
||||
assert t.update((10.0, 13.0, 9.5, 12.9, 1.0, ts)) == pytest.approx(0.0)
|
||||
assert t.update((10.0, 11.0, 9.9, 10.9, 1.0, 5)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_rising_three_methods_reference():
|
||||
t = ta.RisingThreeMethods()
|
||||
assert t.update((10.0, 15.1, 9.9, 15.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((14.0, 14.1, 12.9, 13.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((13.5, 13.6, 12.4, 12.5, 1.0, 2)) == pytest.approx(0.0)
|
||||
assert t.update((13.0, 13.1, 11.9, 12.0, 1.0, 3)) == pytest.approx(0.0)
|
||||
assert t.update((12.5, 16.1, 12.4, 16.0, 1.0, 4)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_falling_three_methods_reference():
|
||||
t = ta.FallingThreeMethods()
|
||||
assert t.update((15.0, 15.1, 9.9, 10.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((11.0, 12.1, 10.9, 12.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((11.5, 12.6, 11.4, 12.5, 1.0, 2)) == pytest.approx(0.0)
|
||||
assert t.update((12.0, 13.1, 11.9, 13.0, 1.0, 3)) == pytest.approx(0.0)
|
||||
assert t.update((12.5, 12.6, 8.9, 9.0, 1.0, 4)) == pytest.approx(-1.0)
|
||||
|
||||
|
||||
def test_upside_gap_three_methods_reference():
|
||||
t = ta.UpsideGapThreeMethods()
|
||||
assert t.update((10.0, 11.2, 9.8, 11.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((12.0, 13.2, 11.9, 13.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((12.5, 12.6, 10.4, 10.5, 1.0, 2)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_downside_gap_three_methods_reference():
|
||||
t = ta.DownsideGapThreeMethods()
|
||||
assert t.update((13.0, 13.2, 11.8, 12.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((11.0, 11.1, 9.8, 10.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((10.5, 12.6, 10.4, 12.5, 1.0, 2)) == pytest.approx(-1.0)
|
||||
|
||||
|
||||
def test_stalled_pattern_reference():
|
||||
t = ta.StalledPattern()
|
||||
assert t.update((10.0, 12.05, 9.9, 12.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((11.0, 14.05, 10.9, 14.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((14.0, 14.6, 13.95, 14.15, 1.0, 2)) == pytest.approx(-1.0)
|
||||
|
||||
|
||||
def test_stick_sandwich_reference():
|
||||
t = ta.StickSandwich()
|
||||
assert t.update((12.0, 12.1, 9.9, 10.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((10.5, 11.6, 10.4, 11.5, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((11.5, 11.6, 9.9, 10.0, 1.0, 2)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_takuri_reference():
|
||||
t = ta.Takuri()
|
||||
assert t.update((10.0, 10.05, 7.0, 10.0, 1.0, 0)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_closing_marubozu_reference():
|
||||
t = ta.ClosingMarubozu()
|
||||
assert t.update((10.5, 15.0, 10.0, 15.0, 1.0, 0)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_opening_marubozu_reference():
|
||||
t = ta.OpeningMarubozu()
|
||||
assert t.update((10.0, 15.0, 10.0, 14.5, 1.0, 0)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_tasuki_gap_reference():
|
||||
t = ta.TasukiGap()
|
||||
assert t.update((10.0, 11.2, 9.8, 11.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((12.0, 14.0, 11.9, 13.5, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((13.0, 13.1, 11.4, 11.5, 1.0, 2)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_unique_three_river_reference():
|
||||
t = ta.UniqueThreeRiver()
|
||||
assert t.update((15.0, 15.1, 10.0, 10.5, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((14.0, 14.1, 9.0, 11.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((10.2, 10.9, 9.5, 10.4, 1.0, 2)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_concealing_baby_swallow_reference():
|
||||
t = ta.ConcealingBabySwallow()
|
||||
assert t.update((20.0, 20.1, 14.9, 15.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((16.0, 16.1, 11.9, 12.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((11.0, 13.0, 9.9, 10.0, 1.0, 2)) == pytest.approx(0.0)
|
||||
assert t.update((14.0, 14.1, 8.9, 9.0, 1.0, 3)) == pytest.approx(1.0)
|
||||
|
||||
# --- Lifecycle ------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -1851,6 +2283,56 @@ def test_three_outside_reference():
|
||||
assert t.update((11.5, 13.0, 11.4, 12.5, 1.0, 2)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_two_crows_reference():
|
||||
t = ta.TwoCrows()
|
||||
assert t.update((10.0, 12.2, 9.9, 12.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((14.0, 14.2, 12.9, 13.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((13.5, 13.6, 10.9, 11.0, 1.0, 2)) == pytest.approx(-1.0)
|
||||
|
||||
|
||||
def test_upside_gap_two_crows_reference():
|
||||
t = ta.UpsideGapTwoCrows()
|
||||
assert t.update((10.0, 12.2, 9.9, 12.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((14.0, 14.2, 12.9, 13.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((15.0, 15.2, 12.4, 12.5, 1.0, 2)) == pytest.approx(-1.0)
|
||||
|
||||
|
||||
def test_identical_three_crows_reference():
|
||||
t = ta.IdenticalThreeCrows()
|
||||
assert t.update((13.0, 13.1, 11.9, 12.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((12.0, 12.1, 10.9, 11.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((11.0, 11.1, 9.9, 10.0, 1.0, 2)) == pytest.approx(-1.0)
|
||||
|
||||
|
||||
def test_three_line_strike_reference():
|
||||
t = ta.ThreeLineStrike()
|
||||
assert t.update((10.0, 11.1, 9.9, 11.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((10.5, 12.1, 10.4, 12.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((11.5, 13.1, 11.4, 13.0, 1.0, 2)) == pytest.approx(0.0)
|
||||
assert t.update((13.5, 13.6, 9.4, 9.5, 1.0, 3)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_three_stars_in_south_reference():
|
||||
t = ta.ThreeStarsInSouth()
|
||||
assert t.update((20.0, 20.1, 8.0, 15.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((18.0, 18.1, 12.0, 16.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((15.0, 15.0, 14.0, 14.0, 1.0, 2)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_abandoned_baby_reference():
|
||||
t = ta.AbandonedBaby()
|
||||
assert t.update((20.0, 20.1, 14.9, 15.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((13.0, 13.1, 12.9, 13.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((16.0, 18.1, 15.9, 18.0, 1.0, 2)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_advance_block_reference():
|
||||
t = ta.AdvanceBlock()
|
||||
assert t.update((10.0, 13.1, 9.9, 13.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((12.0, 14.3, 11.9, 14.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((13.5, 15.0, 13.4, 14.5, 1.0, 2)) == pytest.approx(-1.0)
|
||||
|
||||
|
||||
# --- Lifecycle ------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -1863,3 +2345,213 @@ def test_new_indicators_expose_lifecycle():
|
||||
assert ind.warmup_period() >= 1
|
||||
ind.reset()
|
||||
assert ind.is_ready() is False
|
||||
|
||||
|
||||
def _orderbook_snapshots(n: int) -> list:
|
||||
"""A deterministic varying sequence of order-book snapshots."""
|
||||
snaps = []
|
||||
for i in range(n):
|
||||
bid_sz = 1.0 + (i % 5)
|
||||
ask_sz = 1.0 + ((i + 2) % 4)
|
||||
snaps.append(
|
||||
(
|
||||
[100.0, 99.0],
|
||||
[bid_sz, 1.0],
|
||||
[101.0, 102.0],
|
||||
[ask_sz, 1.0],
|
||||
)
|
||||
)
|
||||
return snaps
|
||||
|
||||
|
||||
def test_orderbook_indicators_streaming_equals_batch():
|
||||
snaps = _orderbook_snapshots(40)
|
||||
for make in (
|
||||
ta.OrderBookImbalanceTop1,
|
||||
lambda: ta.OrderBookImbalanceTopN(2),
|
||||
ta.OrderBookImbalanceFull,
|
||||
ta.Microprice,
|
||||
ta.QuotedSpread,
|
||||
ta.DepthSlope,
|
||||
):
|
||||
batch = make().batch(snaps)
|
||||
streamer = make()
|
||||
streamed = np.array(
|
||||
[streamer.update(*snap) for snap in snaps], dtype=np.float64
|
||||
)
|
||||
assert batch.shape == (len(snaps),)
|
||||
assert _eq_nan(batch, streamed)
|
||||
|
||||
|
||||
def test_tradeflow_indicators_streaming_equals_batch():
|
||||
n = 40
|
||||
price = np.full(n, 100.0)
|
||||
size = np.array([1.0 + (i % 5) for i in range(n)], dtype=np.float64)
|
||||
is_buy = [i % 2 == 0 for i in range(n)]
|
||||
for make in (
|
||||
ta.SignedVolume,
|
||||
ta.CumulativeVolumeDelta,
|
||||
lambda: ta.TradeImbalance(5),
|
||||
):
|
||||
batch = make().batch(price, size, is_buy)
|
||||
streamer = make()
|
||||
streamed = np.array(
|
||||
[streamer.update(price[i], size[i], is_buy[i]) for i in range(n)],
|
||||
dtype=np.float64,
|
||||
)
|
||||
assert batch.shape == (n,)
|
||||
assert _eq_nan(batch, streamed)
|
||||
|
||||
|
||||
def test_price_impact_indicators_streaming_equals_batch():
|
||||
n = 40
|
||||
mid = np.array([100.0 + 0.5 * math.sin(i * 0.4) for i in range(n)], dtype=np.float64)
|
||||
is_buy = [i % 2 == 0 for i in range(n)]
|
||||
# Aggressive trades print across the mid in the aggressor's direction.
|
||||
price = np.array(
|
||||
[mid[i] + (0.02 if is_buy[i] else -0.02) for i in range(n)], dtype=np.float64
|
||||
)
|
||||
size = np.array([1.0 + (i % 5) for i in range(n)], dtype=np.float64)
|
||||
for make in (ta.EffectiveSpread, lambda: ta.RealizedSpread(4), lambda: ta.KylesLambda(5)):
|
||||
batch = make().batch(price, size, is_buy, mid)
|
||||
streamer = make()
|
||||
streamed = np.array(
|
||||
[streamer.update(price[i], size[i], is_buy[i], mid[i]) for i in range(n)],
|
||||
dtype=np.float64,
|
||||
)
|
||||
assert batch.shape == (n,)
|
||||
assert _eq_nan(batch, streamed)
|
||||
|
||||
|
||||
def test_footprint_streaming_equals_batch():
|
||||
n = 20
|
||||
price = [100.0 + (i % 5) * 0.3 for i in range(n)]
|
||||
size = [1.0 + (i % 3) for i in range(n)]
|
||||
is_buy = [i % 2 == 0 for i in range(n)]
|
||||
batch = ta.Footprint(1.0).batch(price, size, is_buy)
|
||||
streamer = ta.Footprint(1.0)
|
||||
assert len(batch) == n
|
||||
for i in range(n):
|
||||
streamed = streamer.update(price[i], size[i], is_buy[i])
|
||||
assert np.array_equal(streamed, batch[i])
|
||||
|
||||
|
||||
def test_funding_indicators_streaming_equals_batch():
|
||||
n = 40
|
||||
rate = np.array([0.0001 * math.sin(i * 0.3) for i in range(n)], dtype=np.float64)
|
||||
for make in (
|
||||
ta.FundingRate,
|
||||
lambda: ta.FundingRateMean(5),
|
||||
lambda: ta.FundingRateZScore(5),
|
||||
):
|
||||
batch = make().batch(rate)
|
||||
streamer = make()
|
||||
streamed = np.array(
|
||||
[streamer.update(rate[i]) for i in range(n)], dtype=np.float64
|
||||
)
|
||||
assert batch.shape == (n,)
|
||||
assert _eq_nan(batch, streamed)
|
||||
|
||||
|
||||
def test_funding_basis_streaming_equals_batch():
|
||||
n = 40
|
||||
index = np.array([100.0 + 0.5 * math.sin(i * 0.2) for i in range(n)], dtype=np.float64)
|
||||
mark = np.array(
|
||||
[index[i] + 0.1 * math.cos(i * 0.3) for i in range(n)], dtype=np.float64
|
||||
)
|
||||
batch = ta.FundingBasis().batch(mark, index)
|
||||
streamer = ta.FundingBasis()
|
||||
streamed = np.array(
|
||||
[streamer.update(mark[i], index[i]) for i in range(n)], dtype=np.float64
|
||||
)
|
||||
assert batch.shape == (n,)
|
||||
assert _eq_nan(batch, streamed)
|
||||
|
||||
|
||||
def test_open_interest_delta_streaming_equals_batch():
|
||||
n = 40
|
||||
oi = np.array([1000.0 + 50.0 * math.sin(i * 0.25) for i in range(n)], dtype=np.float64)
|
||||
batch = ta.OpenInterestDelta().batch(oi)
|
||||
streamer = ta.OpenInterestDelta()
|
||||
streamed = np.array([streamer.update(oi[i]) for i in range(n)], dtype=np.float64)
|
||||
assert batch.shape == (n,)
|
||||
assert _eq_nan(batch, streamed)
|
||||
|
||||
|
||||
def test_oi_flow_indicators_streaming_equals_batch():
|
||||
n = 40
|
||||
oi = np.array([1000.0 + 50.0 * math.sin(i * 0.2) for i in range(n)], dtype=np.float64)
|
||||
mark = np.array([100.0 + math.cos(i * 0.3) for i in range(n)], dtype=np.float64)
|
||||
long_sz = np.array([500.0 + 20.0 * math.sin(i * 0.25) for i in range(n)], dtype=np.float64)
|
||||
short_sz = np.array([400.0 + 20.0 * math.cos(i * 0.25) for i in range(n)], dtype=np.float64)
|
||||
|
||||
# OIPriceDivergence carries a window; update(open_interest, mark_price).
|
||||
batch = ta.OIPriceDivergence(5).batch(oi, mark)
|
||||
streamer = ta.OIPriceDivergence(5)
|
||||
streamed = np.array(
|
||||
[streamer.update(oi[i], mark[i]) for i in range(n)], dtype=np.float64
|
||||
)
|
||||
assert batch.shape == (n,)
|
||||
assert _eq_nan(batch, streamed)
|
||||
|
||||
# OIWeighted; update(mark_price, open_interest).
|
||||
batch = ta.OIWeighted().batch(mark, oi)
|
||||
streamer = ta.OIWeighted()
|
||||
streamed = np.array(
|
||||
[streamer.update(mark[i], oi[i]) for i in range(n)], dtype=np.float64
|
||||
)
|
||||
assert _eq_nan(batch, streamed)
|
||||
|
||||
# LongShortRatio; update(long_size, short_size).
|
||||
batch = ta.LongShortRatio().batch(long_sz, short_sz)
|
||||
streamer = ta.LongShortRatio()
|
||||
streamed = np.array(
|
||||
[streamer.update(long_sz[i], short_sz[i]) for i in range(n)], dtype=np.float64
|
||||
)
|
||||
assert _eq_nan(batch, streamed)
|
||||
|
||||
# TakerBuySellRatio; update(taker_buy_volume, taker_sell_volume).
|
||||
batch = ta.TakerBuySellRatio().batch(long_sz, short_sz)
|
||||
streamer = ta.TakerBuySellRatio()
|
||||
streamed = np.array(
|
||||
[streamer.update(long_sz[i], short_sz[i]) for i in range(n)], dtype=np.float64
|
||||
)
|
||||
assert _eq_nan(batch, streamed)
|
||||
|
||||
|
||||
def test_liquidation_features_streaming_equals_batch():
|
||||
n = 30
|
||||
long_liq = np.array([abs(50.0 * math.sin(i * 0.4)) for i in range(n)], dtype=np.float64)
|
||||
short_liq = np.array([abs(40.0 * math.cos(i * 0.3)) for i in range(n)], dtype=np.float64)
|
||||
batch = ta.LiquidationFeatures().batch(long_liq, short_liq)
|
||||
streamer = ta.LiquidationFeatures()
|
||||
assert batch.shape == (n, 5)
|
||||
for i in range(n):
|
||||
row = streamer.update(long_liq[i], short_liq[i])
|
||||
assert tuple(batch[i]) == pytest.approx(row)
|
||||
|
||||
|
||||
def test_basis_indicators_streaming_equals_batch():
|
||||
n = 40
|
||||
index = np.array([100.0 + math.sin(i * 0.2) for i in range(n)], dtype=np.float64)
|
||||
mark = np.array([index[i] + 0.05 * math.cos(i * 0.3) for i in range(n)], dtype=np.float64)
|
||||
futures = np.array(
|
||||
[index[i] + 0.5 + 0.1 * math.sin(i * 0.25) for i in range(n)], dtype=np.float64
|
||||
)
|
||||
|
||||
# TermStructureBasis; update(futures_price, index_price).
|
||||
batch = ta.TermStructureBasis().batch(futures, index)
|
||||
streamer = ta.TermStructureBasis()
|
||||
streamed = np.array(
|
||||
[streamer.update(futures[i], index[i]) for i in range(n)], dtype=np.float64
|
||||
)
|
||||
assert batch.shape == (n,)
|
||||
assert _eq_nan(batch, streamed)
|
||||
|
||||
# CalendarSpread; update(futures_price, mark_price).
|
||||
batch = ta.CalendarSpread().batch(futures, mark)
|
||||
streamer = ta.CalendarSpread()
|
||||
streamed = np.array(
|
||||
[streamer.update(futures[i], mark[i]) for i in range(n)], dtype=np.float64
|
||||
)
|
||||
assert _eq_nan(batch, streamed)
|
||||
|
||||
@@ -97,3 +97,73 @@ def test_ehlers_super_smoother_batch_shape(sine_prices):
|
||||
def test_mama_batch_shape(sine_prices):
|
||||
out = ta.MAMA().batch(sine_prices)
|
||||
assert out.shape == (sine_prices.size, 2)
|
||||
|
||||
|
||||
def test_orderbook_indicators_construct_and_emit():
|
||||
# All five order-book indicators accept a four-array snapshot and emit a float.
|
||||
snapshot = ([100.0, 99.0], [2.0, 1.0], [101.0, 102.0], [1.0, 1.0])
|
||||
indicators = [
|
||||
ta.OrderBookImbalanceTop1(),
|
||||
ta.OrderBookImbalanceTopN(2),
|
||||
ta.OrderBookImbalanceFull(),
|
||||
ta.Microprice(),
|
||||
ta.QuotedSpread(),
|
||||
ta.DepthSlope(),
|
||||
]
|
||||
for ind in indicators:
|
||||
out = ind.update(*snapshot)
|
||||
assert isinstance(out, float)
|
||||
|
||||
|
||||
def test_orderbook_batch_returns_one_value_per_snapshot():
|
||||
snapshots = [([100.0], [3.0], [101.0], [1.0])] * 5
|
||||
out = ta.OrderBookImbalanceTop1().batch(snapshots)
|
||||
assert out.shape == (5,)
|
||||
assert out.dtype == np.float64
|
||||
|
||||
|
||||
def test_tradeflow_indicators_construct_and_emit():
|
||||
# SignedVolume and CVD emit from the first trade; TradeImbalance(1) too.
|
||||
assert isinstance(ta.SignedVolume().update(100.0, 2.0, True), float)
|
||||
assert isinstance(ta.CumulativeVolumeDelta().update(100.0, 2.0, True), float)
|
||||
assert isinstance(ta.TradeImbalance(1).update(100.0, 2.0, True), float)
|
||||
|
||||
|
||||
def test_tradeflow_batch_returns_one_value_per_trade():
|
||||
price = np.full(6, 100.0)
|
||||
size = np.array([1.0, 2.0, 3.0, 1.0, 2.0, 3.0])
|
||||
is_buy = [True, False, True, False, True, False]
|
||||
out = ta.CumulativeVolumeDelta().batch(price, size, is_buy)
|
||||
assert out.shape == (6,)
|
||||
assert out.dtype == np.float64
|
||||
|
||||
|
||||
def test_price_impact_indicators_construct_and_emit():
|
||||
# Price-impact indicators take a trade paired with the prevailing mid.
|
||||
assert isinstance(ta.EffectiveSpread().update(100.05, 1.0, True, 100.0), float)
|
||||
# RealizedSpread buffers until its horizon elapses.
|
||||
assert ta.RealizedSpread(1).update(100.05, 1.0, True, 100.0) is None
|
||||
|
||||
|
||||
def test_price_impact_batch_returns_one_value_per_trade():
|
||||
price = np.array([100.05, 99.95, 100.10, 99.90])
|
||||
size = np.array([1.0, 2.0, 1.0, 2.0])
|
||||
is_buy = [True, False, True, False]
|
||||
mid = np.full(4, 100.0)
|
||||
for ind in (ta.EffectiveSpread(), ta.RealizedSpread(2), ta.KylesLambda(2)):
|
||||
out = ind.batch(price, size, is_buy, mid)
|
||||
assert out.shape == (4,)
|
||||
assert out.dtype == np.float64
|
||||
|
||||
|
||||
def test_footprint_constructs_and_emits():
|
||||
out = ta.Footprint(1.0).update(100.2, 2.0, True)
|
||||
assert out.shape == (1, 3)
|
||||
assert out.dtype == np.float64
|
||||
|
||||
|
||||
def test_footprint_batch_returns_list_of_arrays():
|
||||
res = ta.Footprint(1.0).batch([100.2, 100.7], [2.0, 3.0], [True, False])
|
||||
assert isinstance(res, list)
|
||||
assert len(res) == 2
|
||||
assert res[-1].shape[1] == 3
|
||||
|
||||
@@ -201,3 +201,50 @@ def test_opening_range_streaming_matches_batch(ohlc_series):
|
||||
rows.append([math.nan, math.nan, math.nan] if out is None else list(out))
|
||||
streamed = np.array(rows, dtype=np.float64)
|
||||
assert _equal_with_nan(batch, streamed)
|
||||
|
||||
|
||||
def test_orderbook_streaming_matches_batch():
|
||||
snaps = [
|
||||
(
|
||||
[100.0, 99.0],
|
||||
[1.0 + (i % 5), 1.0],
|
||||
[101.0, 102.0],
|
||||
[1.0 + ((i + 1) % 3), 1.0],
|
||||
)
|
||||
for i in range(30)
|
||||
]
|
||||
batch = ta.Microprice().batch(snaps)
|
||||
streamer = ta.Microprice()
|
||||
streamed = np.array([streamer.update(*snap) for snap in snaps], dtype=np.float64)
|
||||
assert _equal_with_nan(batch, streamed)
|
||||
|
||||
|
||||
def test_tradeflow_streaming_matches_batch():
|
||||
n = 30
|
||||
price = np.full(n, 100.0)
|
||||
size = np.array([1.0 + (i % 4) for i in range(n)], dtype=np.float64)
|
||||
is_buy = [i % 3 != 0 for i in range(n)]
|
||||
batch = ta.CumulativeVolumeDelta().batch(price, size, is_buy)
|
||||
streamer = ta.CumulativeVolumeDelta()
|
||||
streamed = np.array(
|
||||
[streamer.update(price[i], size[i], is_buy[i]) for i in range(n)],
|
||||
dtype=np.float64,
|
||||
)
|
||||
assert _equal_with_nan(batch, streamed)
|
||||
|
||||
|
||||
def test_price_impact_streaming_matches_batch():
|
||||
n = 30
|
||||
mid = np.array([100.0 + 0.25 * math.sin(i * 0.5) for i in range(n)], dtype=np.float64)
|
||||
is_buy = [i % 3 != 0 for i in range(n)]
|
||||
price = np.array(
|
||||
[mid[i] + (0.03 if is_buy[i] else -0.03) for i in range(n)], dtype=np.float64
|
||||
)
|
||||
size = np.array([1.0 + (i % 4) for i in range(n)], dtype=np.float64)
|
||||
batch = ta.EffectiveSpread().batch(price, size, is_buy, mid)
|
||||
streamer = ta.EffectiveSpread()
|
||||
streamed = np.array(
|
||||
[streamer.update(price[i], size[i], is_buy[i], mid[i]) for i in range(n)],
|
||||
dtype=np.float64,
|
||||
)
|
||||
assert _equal_with_nan(batch, streamed)
|
||||
|
||||
@@ -5,7 +5,7 @@ version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
license-file.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
readme.workspace = true
|
||||
|
||||
+1163
-3
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@ version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
license-file.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
readme.workspace = true
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
//! Derivatives value type: the perpetual / futures tick.
|
||||
//!
|
||||
//! [`DerivativesTick`] is the non-OHLCV input consumed by the derivatives /
|
||||
//! perpetual-futures indicator family. A single tick bundles the funding,
|
||||
//! price, open-interest, positioning, taker-flow and liquidation fields a
|
||||
//! perp/futures venue publishes per update; each indicator reads only the
|
||||
//! subset it needs (the same one-rich-type-per-family pattern as [`Trade`] /
|
||||
//! [`OrderBook`] in [`crate::microstructure`]).
|
||||
//!
|
||||
//! [`Trade`]: crate::microstructure::Trade
|
||||
//! [`OrderBook`]: crate::microstructure::OrderBook
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
/// A single derivatives / perpetual-futures market tick.
|
||||
///
|
||||
/// Field invariants enforced by [`new`](DerivativesTick::new):
|
||||
///
|
||||
/// - `funding_rate` is finite and **may be negative** (a negative funding rate
|
||||
/// means shorts pay longs).
|
||||
/// - `mark_price`, `index_price` and `futures_price` are finite and strictly
|
||||
/// positive.
|
||||
/// - `open_interest`, `long_size`, `short_size`, `taker_buy_volume`,
|
||||
/// `taker_sell_volume`, `long_liquidation` and `short_liquidation` are finite
|
||||
/// and non-negative.
|
||||
///
|
||||
/// `timestamp` is a caller-defined epoch / resolution and is not validated.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct DerivativesTick {
|
||||
/// Current funding rate for the interval (finite; may be negative).
|
||||
pub funding_rate: f64,
|
||||
/// Perpetual mark price (finite, strictly positive).
|
||||
pub mark_price: f64,
|
||||
/// Spot / index price the perpetual tracks (finite, strictly positive).
|
||||
pub index_price: f64,
|
||||
/// Dated (e.g. quarterly) futures mark price (finite, strictly positive).
|
||||
pub futures_price: f64,
|
||||
/// Open interest — outstanding contracts / notional (finite, non-negative).
|
||||
pub open_interest: f64,
|
||||
/// Aggregate long size / long account count (finite, non-negative).
|
||||
pub long_size: f64,
|
||||
/// Aggregate short size / short account count (finite, non-negative).
|
||||
pub short_size: f64,
|
||||
/// Taker buy (ask-lifting) volume (finite, non-negative).
|
||||
pub taker_buy_volume: f64,
|
||||
/// Taker sell (bid-hitting) volume (finite, non-negative).
|
||||
pub taker_sell_volume: f64,
|
||||
/// Long-side liquidation notional (finite, non-negative).
|
||||
pub long_liquidation: f64,
|
||||
/// Short-side liquidation notional (finite, non-negative).
|
||||
pub short_liquidation: f64,
|
||||
/// Tick timestamp (caller-defined epoch / resolution).
|
||||
pub timestamp: i64,
|
||||
}
|
||||
|
||||
impl DerivativesTick {
|
||||
/// Construct a derivatives tick, validating every field invariant.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::InvalidDerivatives`] if `funding_rate` is not finite;
|
||||
/// any of `mark_price`, `index_price`, `futures_price` is not a finite
|
||||
/// positive number; or any of the six size / volume / liquidation fields is
|
||||
/// not a finite non-negative number.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
funding_rate: f64,
|
||||
mark_price: f64,
|
||||
index_price: f64,
|
||||
futures_price: f64,
|
||||
open_interest: f64,
|
||||
long_size: f64,
|
||||
short_size: f64,
|
||||
taker_buy_volume: f64,
|
||||
taker_sell_volume: f64,
|
||||
long_liquidation: f64,
|
||||
short_liquidation: f64,
|
||||
timestamp: i64,
|
||||
) -> Result<Self> {
|
||||
if !funding_rate.is_finite() {
|
||||
return Err(Error::InvalidDerivatives {
|
||||
message: "funding_rate must be finite",
|
||||
});
|
||||
}
|
||||
for price in [mark_price, index_price, futures_price] {
|
||||
if !price.is_finite() || price <= 0.0 {
|
||||
return Err(Error::InvalidDerivatives {
|
||||
message:
|
||||
"mark_price, index_price and futures_price must be finite and positive",
|
||||
});
|
||||
}
|
||||
}
|
||||
for amount in [
|
||||
open_interest,
|
||||
long_size,
|
||||
short_size,
|
||||
taker_buy_volume,
|
||||
taker_sell_volume,
|
||||
long_liquidation,
|
||||
short_liquidation,
|
||||
] {
|
||||
if !amount.is_finite() || amount < 0.0 {
|
||||
return Err(Error::InvalidDerivatives {
|
||||
message: "open interest, sizes, volumes and liquidations must be finite and non-negative",
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(Self {
|
||||
funding_rate,
|
||||
mark_price,
|
||||
index_price,
|
||||
futures_price,
|
||||
open_interest,
|
||||
long_size,
|
||||
short_size,
|
||||
taker_buy_volume,
|
||||
taker_sell_volume,
|
||||
long_liquidation,
|
||||
short_liquidation,
|
||||
timestamp,
|
||||
})
|
||||
}
|
||||
|
||||
/// Construct a derivatives tick without validation. The caller asserts that
|
||||
/// every field invariant documented on [`DerivativesTick`] holds.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[must_use]
|
||||
pub const fn new_unchecked(
|
||||
funding_rate: f64,
|
||||
mark_price: f64,
|
||||
index_price: f64,
|
||||
futures_price: f64,
|
||||
open_interest: f64,
|
||||
long_size: f64,
|
||||
short_size: f64,
|
||||
taker_buy_volume: f64,
|
||||
taker_sell_volume: f64,
|
||||
long_liquidation: f64,
|
||||
short_liquidation: f64,
|
||||
timestamp: i64,
|
||||
) -> Self {
|
||||
Self {
|
||||
funding_rate,
|
||||
mark_price,
|
||||
index_price,
|
||||
futures_price,
|
||||
open_interest,
|
||||
long_size,
|
||||
short_size,
|
||||
taker_buy_volume,
|
||||
taker_sell_volume,
|
||||
long_liquidation,
|
||||
short_liquidation,
|
||||
timestamp,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A fully valid tick used as a baseline; individual tests override one
|
||||
/// field to exercise a single reject branch.
|
||||
fn valid() -> DerivativesTick {
|
||||
DerivativesTick::new(
|
||||
0.0001, 100.0, 99.5, 100.5, 1_000.0, 600.0, 400.0, 50.0, 40.0, 5.0, 3.0, 42,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_accepts_valid() {
|
||||
let tick = valid();
|
||||
assert_eq!(tick.funding_rate, 0.0001);
|
||||
assert_eq!(tick.mark_price, 100.0);
|
||||
assert_eq!(tick.index_price, 99.5);
|
||||
assert_eq!(tick.futures_price, 100.5);
|
||||
assert_eq!(tick.open_interest, 1_000.0);
|
||||
assert_eq!(tick.long_size, 600.0);
|
||||
assert_eq!(tick.short_size, 400.0);
|
||||
assert_eq!(tick.taker_buy_volume, 50.0);
|
||||
assert_eq!(tick.taker_sell_volume, 40.0);
|
||||
assert_eq!(tick.long_liquidation, 5.0);
|
||||
assert_eq!(tick.short_liquidation, 3.0);
|
||||
assert_eq!(tick.timestamp, 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_accepts_negative_funding_and_zero_amounts() {
|
||||
let tick = DerivativesTick::new(
|
||||
-0.0005, 100.0, 100.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(tick.funding_rate, -0.0005);
|
||||
assert_eq!(tick.open_interest, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_rejects_non_finite_funding() {
|
||||
assert!(matches!(
|
||||
DerivativesTick::new(
|
||||
f64::NAN,
|
||||
100.0,
|
||||
100.0,
|
||||
100.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0
|
||||
),
|
||||
Err(Error::InvalidDerivatives { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
DerivativesTick::new(
|
||||
f64::INFINITY,
|
||||
100.0,
|
||||
100.0,
|
||||
100.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0
|
||||
),
|
||||
Err(Error::InvalidDerivatives { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_rejects_non_positive_mark() {
|
||||
assert!(matches!(
|
||||
DerivativesTick::new(0.0, 0.0, 100.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
Err(Error::InvalidDerivatives { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_rejects_non_positive_index() {
|
||||
assert!(matches!(
|
||||
DerivativesTick::new(0.0, 100.0, -1.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
Err(Error::InvalidDerivatives { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_rejects_non_finite_futures() {
|
||||
assert!(matches!(
|
||||
DerivativesTick::new(
|
||||
0.0,
|
||||
100.0,
|
||||
100.0,
|
||||
f64::NAN,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0
|
||||
),
|
||||
Err(Error::InvalidDerivatives { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_rejects_negative_open_interest() {
|
||||
assert!(matches!(
|
||||
DerivativesTick::new(0.0, 100.0, 100.0, 100.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
Err(Error::InvalidDerivatives { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_rejects_non_finite_size() {
|
||||
assert!(matches!(
|
||||
DerivativesTick::new(
|
||||
0.0,
|
||||
100.0,
|
||||
100.0,
|
||||
100.0,
|
||||
0.0,
|
||||
f64::INFINITY,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0
|
||||
),
|
||||
Err(Error::InvalidDerivatives { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_rejects_negative_liquidation() {
|
||||
assert!(matches!(
|
||||
DerivativesTick::new(0.0, 100.0, 100.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -2.0, 0),
|
||||
Err(Error::InvalidDerivatives { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_unchecked_preserves_fields() {
|
||||
let tick = DerivativesTick::new_unchecked(
|
||||
-1.0, -2.0, -3.0, -4.0, -5.0, -6.0, -7.0, -8.0, -9.0, -10.0, -11.0, 7,
|
||||
);
|
||||
assert_eq!(tick.funding_rate, -1.0);
|
||||
assert_eq!(tick.mark_price, -2.0);
|
||||
assert_eq!(tick.short_liquidation, -11.0);
|
||||
assert_eq!(tick.timestamp, 7);
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,27 @@ pub enum Error {
|
||||
/// A multiplier or factor must be strictly positive.
|
||||
#[error("multiplier must be greater than zero")]
|
||||
NonPositiveMultiplier,
|
||||
|
||||
/// An order-book snapshot whose levels do not satisfy the book invariants
|
||||
/// (e.g. a crossed book, non-finite price, negative size, or mis-sorted
|
||||
/// levels) was provided. Order books are a microstructure input distinct
|
||||
/// from candles and ticks, so they surface as their own variant.
|
||||
#[error("invalid order book: {message}")]
|
||||
InvalidOrderBook { message: &'static str },
|
||||
|
||||
/// A trade whose components do not satisfy the trade invariants (e.g.
|
||||
/// non-finite price or negative size) was provided.
|
||||
#[error("invalid trade: {message}")]
|
||||
InvalidTrade { message: &'static str },
|
||||
|
||||
/// A derivatives tick whose components do not satisfy the tick invariants
|
||||
/// (e.g. a non-positive price, a non-finite funding rate, or a negative
|
||||
/// size/volume/liquidation) was provided. Derivatives ticks (funding /
|
||||
/// open-interest / liquidation feeds) are a perpetual-futures input
|
||||
/// distinct from candles, order books and trades, so they surface as their
|
||||
/// own variant.
|
||||
#[error("invalid derivatives tick: {message}")]
|
||||
InvalidDerivatives { message: &'static str },
|
||||
}
|
||||
|
||||
/// Convenience alias for `Result<T, wickra_core::Error>`.
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
//! Abandoned Baby candlestick pattern.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Abandoned Baby — a strong 3-bar reversal where a doji is "abandoned" by price
|
||||
/// gaps on both sides, isolating it from the candles before and after.
|
||||
///
|
||||
/// ```text
|
||||
/// tol = tolerance * max(|bar2.open|, |bar2.close|)
|
||||
/// bar2 doji (|bar2.close − bar2.open| <= tol)
|
||||
///
|
||||
/// bullish (+1.0): bar1 red, bar2 gaps fully below bar1 (bar2.high < bar1.low),
|
||||
/// bar3 green and gaps fully above bar2 (bar3.low > bar2.high)
|
||||
/// bearish (−1.0): bar1 green, bar2 gaps fully above bar1 (bar2.low > bar1.high),
|
||||
/// bar3 red and gaps fully below bar2 (bar3.high < bar2.low)
|
||||
/// ```
|
||||
///
|
||||
/// Output is `0.0` otherwise. The first two bars always return `0.0` because the
|
||||
/// three-bar window is not yet filled. `tolerance` defaults to `0.001` (10 bps
|
||||
/// relative) and bounds how flat the middle candle must be to count as a doji; it
|
||||
/// must lie in `[0, 1)`. Pattern-shape check only — no trend filter is applied;
|
||||
/// combine with a trend indicator for actionable signals.
|
||||
///
|
||||
/// # Signed ±1 encoding
|
||||
///
|
||||
/// This detector emits the uniform candlestick sign convention shared across the
|
||||
/// pattern family — `+1.0` bullish, `−1.0` bearish, `0.0` no pattern — so it
|
||||
/// drops straight into a machine-learning feature matrix where the bullish and
|
||||
/// bearish variants occupy a single dimension.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{AbandonedBaby, Candle, Indicator};
|
||||
///
|
||||
/// let mut indicator = AbandonedBaby::new();
|
||||
/// indicator.update(Candle::new(20.0, 20.1, 14.9, 15.0, 1.0, 0).unwrap());
|
||||
/// indicator.update(Candle::new(13.0, 13.1, 12.9, 13.0, 1.0, 1).unwrap());
|
||||
/// let out = indicator
|
||||
/// .update(Candle::new(16.0, 18.1, 15.9, 18.0, 1.0, 2).unwrap());
|
||||
/// assert_eq!(out, Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AbandonedBaby {
|
||||
tolerance: f64,
|
||||
prev: Option<Candle>,
|
||||
prev_prev: Option<Candle>,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl Default for AbandonedBaby {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl AbandonedBaby {
|
||||
/// Construct a detector with the default relative doji tolerance (1e-3).
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
tolerance: 0.001,
|
||||
prev: None,
|
||||
prev_prev: None,
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a detector with a custom relative doji tolerance.
|
||||
///
|
||||
/// `tolerance` must lie in `[0, 1)`.
|
||||
pub fn with_tolerance(tolerance: f64) -> Result<Self> {
|
||||
if !(0.0..1.0).contains(&tolerance) {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "abandoned baby tolerance must lie in [0, 1)",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
tolerance,
|
||||
prev: None,
|
||||
prev_prev: None,
|
||||
has_emitted: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured relative doji tolerance.
|
||||
pub fn tolerance(&self) -> f64 {
|
||||
self.tolerance
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for AbandonedBaby {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let pp = self.prev_prev;
|
||||
let p = self.prev;
|
||||
self.prev_prev = self.prev;
|
||||
self.prev = Some(candle);
|
||||
let (Some(bar1), Some(bar2)) = (pp, p) else {
|
||||
return Some(0.0);
|
||||
};
|
||||
let tol = self.tolerance * bar2.open.abs().max(bar2.close.abs());
|
||||
let bar2_is_doji = (bar2.close - bar2.open).abs() <= tol;
|
||||
if !bar2_is_doji {
|
||||
return Some(0.0);
|
||||
}
|
||||
// Bullish: red bar1, doji gaps below, green bar3 gaps above.
|
||||
if bar1.close < bar1.open
|
||||
&& bar2.high < bar1.low
|
||||
&& candle.close > candle.open
|
||||
&& candle.low > bar2.high
|
||||
{
|
||||
return Some(1.0);
|
||||
}
|
||||
// Bearish: green bar1, doji gaps above, red bar3 gaps below.
|
||||
if bar1.close > bar1.open
|
||||
&& bar2.low > bar1.high
|
||||
&& candle.close < candle.open
|
||||
&& candle.high < bar2.low
|
||||
{
|
||||
return Some(-1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.prev = None;
|
||||
self.prev_prev = None;
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
3
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"AbandonedBaby"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||
Candle::new(open, high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_tolerance() {
|
||||
assert!(AbandonedBaby::with_tolerance(-0.01).is_err());
|
||||
assert!(AbandonedBaby::with_tolerance(1.0).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_valid_tolerance() {
|
||||
let t = AbandonedBaby::with_tolerance(0.0).unwrap();
|
||||
assert!((t.tolerance() - 0.0).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let t = AbandonedBaby::default();
|
||||
assert_eq!(t.name(), "AbandonedBaby");
|
||||
assert_eq!(t.warmup_period(), 3);
|
||||
assert!(!t.is_ready());
|
||||
assert!((t.tolerance() - 0.001).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bullish_abandoned_baby_is_plus_one() {
|
||||
let mut t = AbandonedBaby::new();
|
||||
assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(13.0, 13.1, 12.9, 13.0, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(16.0, 18.1, 15.9, 18.0, 2)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bearish_abandoned_baby_is_minus_one() {
|
||||
let mut t = AbandonedBaby::new();
|
||||
assert_eq!(t.update(c(15.0, 20.1, 14.9, 20.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(22.0, 22.1, 21.9, 22.0, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(19.0, 19.1, 16.9, 17.0, 2)), Some(-1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn middle_not_doji_yields_zero() {
|
||||
let mut t = AbandonedBaby::new();
|
||||
t.update(c(20.0, 20.1, 14.9, 15.0, 0));
|
||||
// Middle bar has a wide body -> not a doji.
|
||||
assert_eq!(t.update(c(13.0, 14.0, 11.0, 11.5, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(16.0, 18.1, 15.9, 18.0, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_gap_yields_zero() {
|
||||
let mut t = AbandonedBaby::new();
|
||||
t.update(c(20.0, 20.1, 14.9, 15.0, 0));
|
||||
// Doji overlaps bar1's range -> no gap.
|
||||
assert_eq!(t.update(c(15.0, 15.1, 14.9, 15.0, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(16.0, 18.1, 15.9, 18.0, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_two_bars_return_zero() {
|
||||
let mut t = AbandonedBaby::new();
|
||||
assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(13.0, 13.1, 12.9, 13.0, 1)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 100.0 + (i as f64 * 0.3).sin() * 5.0;
|
||||
c(base, base + 1.0, base - 1.0, base + 0.5, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = AbandonedBaby::new();
|
||||
let mut b = AbandonedBaby::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = AbandonedBaby::new();
|
||||
t.update(c(20.0, 20.1, 14.9, 15.0, 0));
|
||||
t.update(c(13.0, 13.1, 12.9, 13.0, 1));
|
||||
t.update(c(16.0, 18.1, 15.9, 18.0, 2));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 0)), Some(0.0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
//! Advance Block candlestick pattern.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Advance Block — a 3-bar bearish warning: three green candles still pushing to
|
||||
/// higher closes, but visibly running out of steam — each real body shrinks while
|
||||
/// the upper shadows lengthen, hinting the advance is about to stall.
|
||||
///
|
||||
/// ```text
|
||||
/// all three green & higher closes
|
||||
/// each opens inside the prior body
|
||||
/// shrinking bodies (body3 < body2 < body1)
|
||||
/// upper shadow of bar3 >= upper shadow of bar2 and bar3 has an upper shadow
|
||||
/// ```
|
||||
///
|
||||
/// Output is `−1.0` when the pattern completes and `0.0` otherwise. Advance Block
|
||||
/// is a single-direction (bearish-only) warning, so it never emits `+1.0`. The
|
||||
/// first two bars always return `0.0` because the three-bar window is not yet
|
||||
/// filled. Pattern-shape check only — no trend filter is applied; combine with a
|
||||
/// trend indicator for actionable signals.
|
||||
///
|
||||
/// # Signed ±1 encoding
|
||||
///
|
||||
/// This detector emits the uniform candlestick sign convention shared across the
|
||||
/// pattern family — `−1.0` bearish, `0.0` no pattern — so it drops straight into
|
||||
/// a machine-learning feature matrix as a single dimension.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{AdvanceBlock, Candle, Indicator};
|
||||
///
|
||||
/// let mut indicator = AdvanceBlock::new();
|
||||
/// indicator.update(Candle::new(10.0, 13.1, 9.9, 13.0, 1.0, 0).unwrap());
|
||||
/// indicator.update(Candle::new(12.0, 14.3, 11.9, 14.0, 1.0, 1).unwrap());
|
||||
/// let out = indicator
|
||||
/// .update(Candle::new(13.5, 15.0, 13.4, 14.5, 1.0, 2).unwrap());
|
||||
/// assert_eq!(out, Some(-1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct AdvanceBlock {
|
||||
prev: Option<Candle>,
|
||||
prev_prev: Option<Candle>,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl AdvanceBlock {
|
||||
/// Construct a new Advance Block detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
prev: None,
|
||||
prev_prev: None,
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for AdvanceBlock {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let pp = self.prev_prev;
|
||||
let p = self.prev;
|
||||
self.prev_prev = self.prev;
|
||||
self.prev = Some(candle);
|
||||
let (Some(bar1), Some(bar2)) = (pp, p) else {
|
||||
return Some(0.0);
|
||||
};
|
||||
let body1 = bar1.close - bar1.open;
|
||||
let body2 = bar2.close - bar2.open;
|
||||
let body3 = candle.close - candle.open;
|
||||
let upper2 = bar2.high - bar2.close;
|
||||
let upper3 = candle.high - candle.close;
|
||||
if bar1.close > bar1.open
|
||||
&& bar2.close > bar2.open
|
||||
&& candle.close > candle.open
|
||||
&& bar2.close > bar1.close
|
||||
&& candle.close > bar2.close
|
||||
&& bar2.open >= bar1.open
|
||||
&& bar2.open <= bar1.close
|
||||
&& candle.open >= bar2.open
|
||||
&& candle.open <= bar2.close
|
||||
&& body2 < body1
|
||||
&& body3 < body2
|
||||
&& upper3 >= upper2
|
||||
&& upper3 > 0.0
|
||||
{
|
||||
return Some(-1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.prev = None;
|
||||
self.prev_prev = None;
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
3
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"AdvanceBlock"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||
Candle::new(open, high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let t = AdvanceBlock::new();
|
||||
assert_eq!(t.name(), "AdvanceBlock");
|
||||
assert_eq!(t.warmup_period(), 3);
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn advance_block_is_minus_one() {
|
||||
let mut t = AdvanceBlock::new();
|
||||
assert_eq!(t.update(c(10.0, 13.1, 9.9, 13.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(12.0, 14.3, 11.9, 14.0, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(13.5, 15.0, 13.4, 14.5, 2)), Some(-1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strong_advance_yields_zero() {
|
||||
let mut t = AdvanceBlock::new();
|
||||
// Bodies grow instead of shrinking -> a strong advance, not blocked.
|
||||
assert_eq!(t.update(c(10.0, 11.1, 9.9, 11.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(10.5, 12.6, 10.4, 12.5, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(11.5, 14.1, 11.4, 14.0, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_upper_shadow_growth_yields_zero() {
|
||||
let mut t = AdvanceBlock::new();
|
||||
t.update(c(10.0, 13.1, 9.9, 13.0, 0));
|
||||
t.update(c(12.0, 14.3, 11.9, 14.0, 1));
|
||||
// bar3 shrinking body but no upper shadow -> not blocked.
|
||||
assert_eq!(t.update(c(13.5, 14.5, 13.4, 14.5, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_two_bars_return_zero() {
|
||||
let mut t = AdvanceBlock::new();
|
||||
assert_eq!(t.update(c(10.0, 13.1, 9.9, 13.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(12.0, 14.3, 11.9, 14.0, 1)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 100.0 + i as f64;
|
||||
c(base, base + 2.0, base - 0.2, base + 1.5, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = AdvanceBlock::new();
|
||||
let mut b = AdvanceBlock::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = AdvanceBlock::new();
|
||||
t.update(c(10.0, 13.1, 9.9, 13.0, 0));
|
||||
t.update(c(12.0, 14.3, 11.9, 14.0, 1));
|
||||
t.update(c(13.5, 15.0, 13.4, 14.5, 2));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.update(c(10.0, 13.1, 9.9, 13.0, 0)), Some(0.0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
//! Belt-hold candlestick pattern.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Belt-hold — a single-bar reversal: a long candle that opens at one extreme of
|
||||
/// its range (an "opening marubozu") and runs the other way.
|
||||
///
|
||||
/// ```text
|
||||
/// range = high − low
|
||||
/// bullish (+1.0): green, opens at the low (open − low <= tol * range) & long body
|
||||
/// bearish (−1.0): red, opens at the high (high − open <= tol * range) & long body
|
||||
/// long body = |close − open| >= 0.5 * range
|
||||
/// ```
|
||||
///
|
||||
/// Output is `0.0` when the opening side carries a shadow, the body is short, or
|
||||
/// the range is degenerate. `shadow_tolerance` defaults to `0.05` (5 % of the bar
|
||||
/// range allowed on the opening side) and must lie in `[0, 1)`. Pattern-shape
|
||||
/// check only — no trend filter is applied; combine with a trend indicator for
|
||||
/// actionable signals.
|
||||
///
|
||||
/// # Signed ±1 encoding
|
||||
///
|
||||
/// This detector emits the uniform candlestick sign convention shared across the
|
||||
/// pattern family — `+1.0` bullish, `−1.0` bearish, `0.0` no pattern — so it
|
||||
/// drops straight into a machine-learning feature matrix where the bullish and
|
||||
/// bearish variants occupy a single dimension.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{BeltHold, Candle, Indicator};
|
||||
///
|
||||
/// let mut indicator = BeltHold::new();
|
||||
/// // Bullish belt-hold: opens at the low, closes near the high.
|
||||
/// let candle = Candle::new(10.0, 12.0, 10.0, 11.5, 1.0, 0).unwrap();
|
||||
/// assert_eq!(indicator.update(candle), Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BeltHold {
|
||||
shadow_tolerance: f64,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl Default for BeltHold {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl BeltHold {
|
||||
/// Construct a Belt-hold detector with the default 5 % opening-shadow tolerance.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
shadow_tolerance: 0.05,
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a Belt-hold detector with a custom opening-shadow tolerance.
|
||||
///
|
||||
/// `shadow_tolerance` must lie in `[0, 1)`.
|
||||
pub fn with_tolerance(shadow_tolerance: f64) -> Result<Self> {
|
||||
if !(0.0..1.0).contains(&shadow_tolerance) {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "belt-hold shadow tolerance must lie in [0, 1)",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
shadow_tolerance,
|
||||
has_emitted: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured opening-shadow tolerance.
|
||||
pub fn shadow_tolerance(&self) -> f64 {
|
||||
self.shadow_tolerance
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for BeltHold {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let range = candle.high - candle.low;
|
||||
if range <= 0.0 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let body = candle.close - candle.open;
|
||||
if body.abs() < 0.5 * range {
|
||||
return Some(0.0);
|
||||
}
|
||||
let tol = self.shadow_tolerance * range;
|
||||
// Bullish: opens at the low (no lower shadow), green body.
|
||||
if body > 0.0 && candle.open - candle.low <= tol {
|
||||
return Some(1.0);
|
||||
}
|
||||
// Bearish: opens at the high (no upper shadow), red body.
|
||||
if body < 0.0 && candle.high - candle.open <= tol {
|
||||
return Some(-1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"BeltHold"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||
Candle::new(open, high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_tolerance() {
|
||||
assert!(BeltHold::with_tolerance(-0.01).is_err());
|
||||
assert!(BeltHold::with_tolerance(1.0).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_valid_tolerance() {
|
||||
let t = BeltHold::with_tolerance(0.0).unwrap();
|
||||
assert!((t.shadow_tolerance() - 0.0).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let t = BeltHold::default();
|
||||
assert_eq!(t.name(), "BeltHold");
|
||||
assert_eq!(t.warmup_period(), 1);
|
||||
assert!(!t.is_ready());
|
||||
assert!((t.shadow_tolerance() - 0.05).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bullish_belt_hold_is_plus_one() {
|
||||
let mut t = BeltHold::new();
|
||||
assert_eq!(t.update(c(10.0, 12.0, 10.0, 11.5, 0)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bearish_belt_hold_is_minus_one() {
|
||||
let mut t = BeltHold::new();
|
||||
assert_eq!(t.update(c(12.0, 12.0, 10.0, 10.5, 0)), Some(-1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opening_shadow_yields_zero() {
|
||||
let mut t = BeltHold::new();
|
||||
// Opens 0.5 above the low -> lower shadow exceeds tolerance.
|
||||
assert_eq!(t.update(c(10.5, 12.0, 10.0, 11.5, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_body_yields_zero() {
|
||||
let mut t = BeltHold::new();
|
||||
// Body 0.5 < half the range (1.0) -> not a long belt-hold.
|
||||
assert_eq!(t.update(c(10.0, 12.0, 10.0, 10.5, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_range_yields_zero() {
|
||||
let mut t = BeltHold::new();
|
||||
assert_eq!(t.update(c(10.0, 10.0, 10.0, 10.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 100.0 + i as f64;
|
||||
c(base, base + 2.0, base, base + 1.8, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = BeltHold::new();
|
||||
let mut b = BeltHold::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = BeltHold::new();
|
||||
t.update(c(10.0, 12.0, 10.0, 11.5, 0));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
//! Breakaway candlestick pattern.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Breakaway — a 5-bar reversal that fades an exhausted run. A trend gaps away on
|
||||
/// the second bar, drifts two more bars in the same direction, then the fifth bar
|
||||
/// snaps the other way and closes back inside the body gap left between the first
|
||||
/// and second bars, signalling the move has broken away from the crowd and is
|
||||
/// turning.
|
||||
///
|
||||
/// ```text
|
||||
/// bullish (+1.0) — appears in a decline:
|
||||
/// bar1 black (close < open)
|
||||
/// bar2 black & its body gaps DOWN below bar1's body (bar2.open < bar1.close)
|
||||
/// bar3 extends lower (high & low below bar2)
|
||||
/// bar4 black & extends lower (high & low below bar3)
|
||||
/// bar5 green & closes inside the bar1/bar2 body gap (bar2.open < close < bar1.close)
|
||||
///
|
||||
/// bearish (−1.0) — the mirror in an advance:
|
||||
/// bar1 white (close > open)
|
||||
/// bar2 white & its body gaps UP above bar1's body (bar2.open > bar1.close)
|
||||
/// bar3 extends higher (high & low above bar2)
|
||||
/// bar4 white & extends higher (high & low above bar3)
|
||||
/// bar5 red & closes inside the bar1/bar2 body gap (bar1.close < close < bar2.open)
|
||||
/// ```
|
||||
///
|
||||
/// The middle bar (`bar3`) may be either colour — only its high/low must extend
|
||||
/// the run. Output is `+1.0` bullish, `−1.0` bearish, `0.0` otherwise. The first
|
||||
/// four bars always return `0.0` because the five-bar window is not yet filled.
|
||||
/// Pattern-shape check only — no trend filter is applied; combine with a trend
|
||||
/// indicator for actionable signals. Recognition uses TA-Lib's
|
||||
/// `CDLBREAKAWAY` body-gap and high/low ordering rules directly; it does not add
|
||||
/// TA-Lib's rolling body-length average, matching the geometric house style of
|
||||
/// the other multi-bar patterns in this family.
|
||||
///
|
||||
/// # Signed ±1 encoding
|
||||
///
|
||||
/// This detector emits the uniform candlestick sign convention shared across the
|
||||
/// pattern family — `+1.0` bullish, `−1.0` bearish, `0.0` no pattern — so it
|
||||
/// drops straight into a machine-learning feature matrix where the bullish and
|
||||
/// bearish variants occupy a single dimension.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Breakaway, Candle, Indicator};
|
||||
///
|
||||
/// let mut indicator = Breakaway::new();
|
||||
/// indicator.update(Candle::new(20.0, 20.2, 14.8, 15.0, 1.0, 0).unwrap());
|
||||
/// indicator.update(Candle::new(14.0, 14.1, 11.9, 12.0, 1.0, 1).unwrap());
|
||||
/// indicator.update(Candle::new(12.5, 13.0, 10.5, 11.0, 1.0, 2).unwrap());
|
||||
/// indicator.update(Candle::new(11.0, 11.5, 9.0, 9.5, 1.0, 3).unwrap());
|
||||
/// let out = indicator
|
||||
/// .update(Candle::new(9.5, 14.7, 9.4, 14.5, 1.0, 4).unwrap());
|
||||
/// assert_eq!(out, Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Breakaway {
|
||||
c1: Option<Candle>,
|
||||
c2: Option<Candle>,
|
||||
c3: Option<Candle>,
|
||||
c4: Option<Candle>,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl Breakaway {
|
||||
/// Construct a new Breakaway detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
c1: None,
|
||||
c2: None,
|
||||
c3: None,
|
||||
c4: None,
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for Breakaway {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let bar1 = self.c1;
|
||||
let bar2 = self.c2;
|
||||
let bar3 = self.c3;
|
||||
let bar4 = self.c4;
|
||||
self.c1 = self.c2;
|
||||
self.c2 = self.c3;
|
||||
self.c3 = self.c4;
|
||||
self.c4 = Some(candle);
|
||||
let (Some(bar1), Some(bar2), Some(bar3), Some(bar4)) = (bar1, bar2, bar3, bar4) else {
|
||||
return Some(0.0);
|
||||
};
|
||||
// Bullish: a decline gaps lower, runs two more bars down, then a green
|
||||
// bar5 closes back inside the bar1/bar2 body gap.
|
||||
if bar1.close < bar1.open
|
||||
&& bar2.close < bar2.open
|
||||
&& bar2.open < bar1.close
|
||||
&& bar3.high < bar2.high
|
||||
&& bar3.low < bar2.low
|
||||
&& bar4.close < bar4.open
|
||||
&& bar4.high < bar3.high
|
||||
&& bar4.low < bar3.low
|
||||
&& candle.close > candle.open
|
||||
&& candle.close > bar2.open
|
||||
&& candle.close < bar1.close
|
||||
{
|
||||
return Some(1.0);
|
||||
}
|
||||
// Bearish: the mirror — an advance gaps higher, runs two more bars up,
|
||||
// then a red bar5 closes back inside the bar1/bar2 body gap.
|
||||
if bar1.close > bar1.open
|
||||
&& bar2.close > bar2.open
|
||||
&& bar2.open > bar1.close
|
||||
&& bar3.high > bar2.high
|
||||
&& bar3.low > bar2.low
|
||||
&& bar4.close > bar4.open
|
||||
&& bar4.high > bar3.high
|
||||
&& bar4.low > bar3.low
|
||||
&& candle.close < candle.open
|
||||
&& candle.close < bar2.open
|
||||
&& candle.close > bar1.close
|
||||
{
|
||||
return Some(-1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.c1 = None;
|
||||
self.c2 = None;
|
||||
self.c3 = None;
|
||||
self.c4 = None;
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
5
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"Breakaway"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||
Candle::new(open, high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let t = Breakaway::new();
|
||||
assert_eq!(t.name(), "Breakaway");
|
||||
assert_eq!(t.warmup_period(), 5);
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bullish_breakaway_is_plus_one() {
|
||||
let mut t = Breakaway::new();
|
||||
assert_eq!(t.update(c(20.0, 20.2, 14.8, 15.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(14.0, 14.1, 11.9, 12.0, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(12.5, 13.0, 10.5, 11.0, 2)), Some(0.0));
|
||||
assert_eq!(t.update(c(11.0, 11.5, 9.0, 9.5, 3)), Some(0.0));
|
||||
assert_eq!(t.update(c(9.5, 14.7, 9.4, 14.5, 4)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bearish_breakaway_is_minus_one() {
|
||||
let mut t = Breakaway::new();
|
||||
assert_eq!(t.update(c(15.0, 20.2, 14.8, 20.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(21.0, 23.1, 20.9, 23.0, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(22.5, 24.5, 21.5, 24.0, 2)), Some(0.0));
|
||||
assert_eq!(t.update(c(24.0, 26.5, 23.0, 26.0, 3)), Some(0.0));
|
||||
assert_eq!(t.update(c(27.0, 27.2, 20.4, 20.5, 4)), Some(-1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_body_gap_yields_zero() {
|
||||
let mut t = Breakaway::new();
|
||||
// bar2 does not gap below bar1's body (bar2.open >= bar1.close).
|
||||
t.update(c(20.0, 20.2, 14.8, 15.0, 0));
|
||||
t.update(c(16.0, 16.1, 13.9, 14.0, 1));
|
||||
t.update(c(13.5, 14.0, 11.5, 12.0, 2));
|
||||
t.update(c(12.0, 12.5, 10.0, 10.5, 3));
|
||||
assert_eq!(t.update(c(10.5, 15.7, 10.4, 15.5, 4)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bullish_close_outside_gap_yields_zero() {
|
||||
let mut t = Breakaway::new();
|
||||
t.update(c(20.0, 20.2, 14.8, 15.0, 0));
|
||||
t.update(c(14.0, 14.1, 11.9, 12.0, 1));
|
||||
t.update(c(12.5, 13.0, 10.5, 11.0, 2));
|
||||
t.update(c(11.0, 11.5, 9.0, 9.5, 3));
|
||||
// bar5 closes at 13.0 — below bar2.open (14), so outside the body gap.
|
||||
assert_eq!(t.update(c(9.5, 13.2, 9.4, 13.0, 4)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_four_bars_return_zero() {
|
||||
let mut t = Breakaway::new();
|
||||
assert_eq!(t.update(c(20.0, 20.2, 14.8, 15.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(14.0, 14.1, 11.9, 12.0, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(12.5, 13.0, 10.5, 11.0, 2)), Some(0.0));
|
||||
assert_eq!(t.update(c(11.0, 11.5, 9.0, 9.5, 3)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 100.0 + i as f64;
|
||||
c(base, base + 2.0, base - 0.5, base + 1.5, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = Breakaway::new();
|
||||
let mut b = Breakaway::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = Breakaway::new();
|
||||
t.update(c(20.0, 20.2, 14.8, 15.0, 0));
|
||||
t.update(c(14.0, 14.1, 11.9, 12.0, 1));
|
||||
t.update(c(12.5, 13.0, 10.5, 11.0, 2));
|
||||
t.update(c(11.0, 11.5, 9.0, 9.5, 3));
|
||||
t.update(c(9.5, 14.7, 9.4, 14.5, 4));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.update(c(20.0, 20.2, 14.8, 15.0, 0)), Some(0.0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
//! Calendar Spread — the dated future's relative premium to the perpetual.
|
||||
|
||||
use crate::derivatives::DerivativesTick;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Calendar Spread — the relative spread between a dated (e.g. quarterly)
|
||||
/// futures price and the perpetual mark price.
|
||||
///
|
||||
/// ```text
|
||||
/// spread = (futuresPrice − markPrice) / markPrice
|
||||
/// ```
|
||||
///
|
||||
/// A calendar (or inter-delivery) spread trades the *near* leg against the
|
||||
/// *far* leg — here the perpetual against a dated future. The relative spread is
|
||||
/// the roll yield available between the two contracts: positive when the future
|
||||
/// trades over the perpetual (contango roll), negative when under
|
||||
/// (backwardation). Where [`TermStructureBasis`] measures the future against
|
||||
/// spot, this measures it against the perpetual — the leg a perp-vs-future
|
||||
/// basis trade actually holds. The output is a fraction; multiply by `10_000`
|
||||
/// for basis points.
|
||||
///
|
||||
/// `Input = DerivativesTick`, `Output = f64`. Stateless; ready after the first
|
||||
/// tick.
|
||||
///
|
||||
/// [`TermStructureBasis`]: crate::TermStructureBasis
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{CalendarSpread, DerivativesTick, Indicator};
|
||||
///
|
||||
/// fn tick(futures: f64, mark: f64) -> DerivativesTick {
|
||||
/// DerivativesTick::new(0.0, mark, mark, futures, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0)
|
||||
/// .unwrap()
|
||||
/// }
|
||||
///
|
||||
/// let mut cs = CalendarSpread::new();
|
||||
/// // futures 101 vs perpetual mark 100 -> 0.01.
|
||||
/// assert!((cs.update(tick(101.0, 100.0)).unwrap() - 0.01).abs() < 1e-12);
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct CalendarSpread {
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl CalendarSpread {
|
||||
/// Construct a new calendar-spread indicator.
|
||||
#[must_use]
|
||||
pub const fn new() -> Self {
|
||||
Self { has_emitted: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for CalendarSpread {
|
||||
type Input = DerivativesTick;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, tick: DerivativesTick) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
Some((tick.futures_price - tick.mark_price) / tick.mark_price)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"CalendarSpread"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn tick(futures: f64, mark: f64) -> DerivativesTick {
|
||||
DerivativesTick::new_unchecked(
|
||||
0.0, mark, mark, futures, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let cs = CalendarSpread::new();
|
||||
assert_eq!(cs.name(), "CalendarSpread");
|
||||
assert_eq!(cs.warmup_period(), 1);
|
||||
assert!(!cs.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn future_over_perp_is_positive() {
|
||||
let mut cs = CalendarSpread::new();
|
||||
let out = cs.update(tick(101.0, 100.0)).unwrap();
|
||||
assert!((out - 0.01).abs() < 1e-12);
|
||||
assert!(cs.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn future_under_perp_is_negative() {
|
||||
let mut cs = CalendarSpread::new();
|
||||
let out = cs.update(tick(99.0, 100.0)).unwrap();
|
||||
assert!((out + 0.01).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flat_is_zero() {
|
||||
let mut cs = CalendarSpread::new();
|
||||
assert_eq!(cs.update(tick(100.0, 100.0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let ticks: Vec<DerivativesTick> = (0..20)
|
||||
.map(|i| tick(100.0 + f64::from(i % 5), 100.0))
|
||||
.collect();
|
||||
let mut a = CalendarSpread::new();
|
||||
let mut b = CalendarSpread::new();
|
||||
assert_eq!(
|
||||
a.batch(&ticks),
|
||||
ticks.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut cs = CalendarSpread::new();
|
||||
cs.update(tick(101.0, 100.0));
|
||||
assert!(cs.is_ready());
|
||||
cs.reset();
|
||||
assert!(!cs.is_ready());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
//! Closing Marubozu candlestick pattern.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Closing Marubozu — a single-bar strong-momentum candle with a long body and no
|
||||
/// shadow on the *close* end. A white closing marubozu closes right at the high
|
||||
/// (no upper shadow) and may carry an opening shadow below; a black one closes
|
||||
/// right at the low (no lower shadow) and may carry an opening shadow above. The
|
||||
/// shaved close end shows the move ran unopposed into the bell.
|
||||
///
|
||||
/// ```text
|
||||
/// range = high − low
|
||||
/// long body: |close − open| >= 0.7 * range
|
||||
/// white: close > open and high − close <= 0.05 * range (close at the high)
|
||||
/// black: close < open and close − low <= 0.05 * range (close at the low)
|
||||
/// ```
|
||||
///
|
||||
/// Output is `+1.0` for a white closing marubozu, `−1.0` for a black one, and
|
||||
/// `0.0` otherwise. Body and shadow thresholds follow the geometric house style
|
||||
/// rather than TA-Lib's rolling averages. The opposite shaved end is
|
||||
/// [`crate::OpeningMarubozu`]. Pattern-shape check only — no trend filter is
|
||||
/// applied; combine with a trend indicator for actionable signals.
|
||||
///
|
||||
/// # Signed ±1 encoding
|
||||
///
|
||||
/// This detector emits the uniform candlestick sign convention shared across the
|
||||
/// pattern family — `+1.0` bullish, `−1.0` bearish, `0.0` no pattern — so it drops
|
||||
/// straight into a machine-learning feature matrix where the bullish and bearish
|
||||
/// variants occupy a single dimension.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, ClosingMarubozu, Indicator};
|
||||
///
|
||||
/// let mut indicator = ClosingMarubozu::new();
|
||||
/// // White: closes at the high, small opening shadow below.
|
||||
/// let candle = Candle::new(10.5, 15.0, 10.0, 15.0, 1.0, 0).unwrap();
|
||||
/// assert_eq!(indicator.update(candle), Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ClosingMarubozu {
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl ClosingMarubozu {
|
||||
/// Construct a new Closing Marubozu detector.
|
||||
pub const fn new() -> Self {
|
||||
Self { has_emitted: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for ClosingMarubozu {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let range = candle.high - candle.low;
|
||||
if range <= 0.0 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let body = candle.close - candle.open;
|
||||
if body.abs() < 0.7 * range {
|
||||
return Some(0.0);
|
||||
}
|
||||
let tol = 0.05 * range;
|
||||
if body > 0.0 && candle.high - candle.close <= tol {
|
||||
return Some(1.0);
|
||||
}
|
||||
if body < 0.0 && candle.close - candle.low <= tol {
|
||||
return Some(-1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"ClosingMarubozu"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||
Candle::new(open, high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let t = ClosingMarubozu::new();
|
||||
assert_eq!(t.name(), "ClosingMarubozu");
|
||||
assert_eq!(t.warmup_period(), 1);
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_closing_marubozu_is_plus_one() {
|
||||
let mut t = ClosingMarubozu::new();
|
||||
// Closes at the high, opening shadow below.
|
||||
assert_eq!(t.update(c(10.5, 15.0, 10.0, 15.0, 0)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn black_closing_marubozu_is_minus_one() {
|
||||
let mut t = ClosingMarubozu::new();
|
||||
// Closes at the low, opening shadow above.
|
||||
assert_eq!(t.update(c(14.5, 15.0, 10.0, 10.0, 0)), Some(-1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_with_upper_shadow_yields_zero() {
|
||||
let mut t = ClosingMarubozu::new();
|
||||
// Long white body but a clear upper shadow -> close is not at the high.
|
||||
assert_eq!(t.update(c(10.5, 16.0, 10.0, 15.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn black_with_lower_shadow_yields_zero() {
|
||||
let mut t = ClosingMarubozu::new();
|
||||
// Long black body but a clear lower shadow -> close is not at the low.
|
||||
assert_eq!(t.update(c(14.5, 15.0, 9.0, 10.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_body_yields_zero() {
|
||||
let mut t = ClosingMarubozu::new();
|
||||
// Body is short relative to range.
|
||||
assert_eq!(t.update(c(12.0, 15.0, 10.0, 12.5, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_range_yields_zero() {
|
||||
let mut t = ClosingMarubozu::new();
|
||||
assert_eq!(t.update(c(10.0, 10.0, 10.0, 10.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 100.0 + i as f64;
|
||||
c(base + 0.5, base + 5.0, base, base + 5.0, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = ClosingMarubozu::new();
|
||||
let mut b = ClosingMarubozu::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = ClosingMarubozu::new();
|
||||
t.update(c(10.5, 15.0, 10.0, 15.0, 0));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
//! Concealing Baby Swallow candlestick pattern.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Returns `true` when `candle` is a black marubozu: a down candle whose body fills
|
||||
/// the range with negligible shadows on both ends.
|
||||
fn black_marubozu(candle: Candle) -> bool {
|
||||
let range = candle.high - candle.low;
|
||||
if range <= 0.0 {
|
||||
return false;
|
||||
}
|
||||
let upper = candle.high - candle.open;
|
||||
let lower = candle.close - candle.low;
|
||||
candle.open > candle.close && upper <= 0.05 * range && lower <= 0.05 * range
|
||||
}
|
||||
|
||||
/// Concealing Baby Swallow — a rare 4-bar bullish reversal. Two black marubozu lead
|
||||
/// a steep decline; the third is a black candle that gaps down on the open yet
|
||||
/// throws a long upper shadow back up into the second body; the fourth is a large
|
||||
/// black candle that completely engulfs the third, shadows included. The relentless
|
||||
/// selling that can no longer make ground signals capitulation.
|
||||
///
|
||||
/// ```text
|
||||
/// bar1, bar2 black marubozu (body == range, negligible shadows)
|
||||
/// bar3 black, opens below bar2's body (open3 < close2) with an upper
|
||||
/// shadow into it (high3 > close2)
|
||||
/// bar4 black, engulfs bar3 including shadows: open4 > high3 and close4 < low3
|
||||
/// ```
|
||||
///
|
||||
/// Output is `+1.0` when the pattern completes and `0.0` otherwise. Concealing Baby
|
||||
/// Swallow is a single-direction (bullish-only) reversal, so it never emits `−1.0`.
|
||||
/// The first three bars always return `0.0` because the four-bar window is not yet
|
||||
/// filled. Body and shadow thresholds follow the geometric house style rather than
|
||||
/// TA-Lib's rolling averages. Pattern-shape check only — no trend filter is applied;
|
||||
/// combine with a trend indicator for actionable signals.
|
||||
///
|
||||
/// # Signed ±1 encoding
|
||||
///
|
||||
/// This detector emits the uniform candlestick sign convention shared across the
|
||||
/// pattern family — `+1.0` bullish, `0.0` no pattern — so it drops straight into
|
||||
/// a machine-learning feature matrix as a single dimension.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, ConcealingBabySwallow, Indicator};
|
||||
///
|
||||
/// let mut indicator = ConcealingBabySwallow::new();
|
||||
/// indicator.update(Candle::new(20.0, 20.1, 14.9, 15.0, 1.0, 0).unwrap());
|
||||
/// indicator.update(Candle::new(16.0, 16.1, 11.9, 12.0, 1.0, 1).unwrap());
|
||||
/// indicator.update(Candle::new(11.0, 13.0, 9.9, 10.0, 1.0, 2).unwrap());
|
||||
/// let out = indicator
|
||||
/// .update(Candle::new(14.0, 14.1, 8.9, 9.0, 1.0, 3).unwrap());
|
||||
/// assert_eq!(out, Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ConcealingBabySwallow {
|
||||
c1: Option<Candle>,
|
||||
c2: Option<Candle>,
|
||||
c3: Option<Candle>,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl ConcealingBabySwallow {
|
||||
/// Construct a new Concealing Baby Swallow detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
c1: None,
|
||||
c2: None,
|
||||
c3: None,
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for ConcealingBabySwallow {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let bar1 = self.c1;
|
||||
let bar2 = self.c2;
|
||||
let bar3 = self.c3;
|
||||
self.c1 = self.c2;
|
||||
self.c2 = self.c3;
|
||||
self.c3 = Some(candle);
|
||||
let (Some(bar1), Some(bar2), Some(bar3)) = (bar1, bar2, bar3) else {
|
||||
return Some(0.0);
|
||||
};
|
||||
// bar1 and bar2 are black marubozu.
|
||||
if !black_marubozu(bar1) || !black_marubozu(bar2) {
|
||||
return Some(0.0);
|
||||
}
|
||||
// bar3 is black, gaps down on the open, throws an upper shadow into bar2.
|
||||
if bar3.open <= bar3.close {
|
||||
return Some(0.0);
|
||||
}
|
||||
if bar3.open >= bar2.close {
|
||||
return Some(0.0); // no downside open gap
|
||||
}
|
||||
if bar3.high <= bar2.close {
|
||||
return Some(0.0); // upper shadow does not reach into bar2's body
|
||||
}
|
||||
// bar4 is black and engulfs bar3 including its shadows.
|
||||
if candle.open <= candle.close {
|
||||
return Some(0.0);
|
||||
}
|
||||
if candle.open > bar3.high && candle.close < bar3.low {
|
||||
return Some(1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.c1 = None;
|
||||
self.c2 = None;
|
||||
self.c3 = None;
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
4
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"ConcealingBabySwallow"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||
Candle::new(open, high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let t = ConcealingBabySwallow::new();
|
||||
assert_eq!(t.name(), "ConcealingBabySwallow");
|
||||
assert_eq!(t.warmup_period(), 4);
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concealing_baby_swallow_is_plus_one() {
|
||||
let mut t = ConcealingBabySwallow::new();
|
||||
assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(16.0, 16.1, 11.9, 12.0, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(11.0, 13.0, 9.9, 10.0, 2)), Some(0.0));
|
||||
assert_eq!(t.update(c(14.0, 14.1, 8.9, 9.0, 3)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warmup_returns_zero() {
|
||||
let mut t = ConcealingBabySwallow::new();
|
||||
assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(16.0, 16.1, 11.9, 12.0, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(11.0, 13.0, 9.9, 10.0, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_bar_not_marubozu_yields_zero() {
|
||||
let mut t = ConcealingBabySwallow::new();
|
||||
// bar1 white.
|
||||
t.update(c(15.0, 20.1, 14.9, 20.0, 0));
|
||||
t.update(c(16.0, 16.1, 11.9, 12.0, 1));
|
||||
t.update(c(11.0, 13.0, 9.9, 10.0, 2));
|
||||
assert_eq!(t.update(c(14.0, 14.1, 8.9, 9.0, 3)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_bar_zero_range_yields_zero() {
|
||||
let mut t = ConcealingBabySwallow::new();
|
||||
// bar1 zero range -> not a marubozu.
|
||||
t.update(c(15.0, 15.0, 15.0, 15.0, 0));
|
||||
t.update(c(16.0, 16.1, 11.9, 12.0, 1));
|
||||
t.update(c(11.0, 13.0, 9.9, 10.0, 2));
|
||||
assert_eq!(t.update(c(14.0, 14.1, 8.9, 9.0, 3)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn second_bar_not_marubozu_yields_zero() {
|
||||
let mut t = ConcealingBabySwallow::new();
|
||||
t.update(c(20.0, 20.1, 14.9, 15.0, 0));
|
||||
// bar2 white.
|
||||
t.update(c(12.0, 16.1, 11.9, 16.0, 1));
|
||||
t.update(c(11.0, 13.0, 9.9, 10.0, 2));
|
||||
assert_eq!(t.update(c(14.0, 14.1, 8.9, 9.0, 3)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn third_bar_not_black_yields_zero() {
|
||||
let mut t = ConcealingBabySwallow::new();
|
||||
t.update(c(20.0, 20.1, 14.9, 15.0, 0));
|
||||
t.update(c(16.0, 16.1, 11.9, 12.0, 1));
|
||||
// bar3 white.
|
||||
t.update(c(11.0, 13.0, 9.9, 12.5, 2));
|
||||
assert_eq!(t.update(c(14.0, 14.1, 8.9, 9.0, 3)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn third_bar_no_gap_yields_zero() {
|
||||
let mut t = ConcealingBabySwallow::new();
|
||||
t.update(c(20.0, 20.1, 14.9, 15.0, 0));
|
||||
t.update(c(16.0, 16.1, 11.9, 12.0, 1));
|
||||
// bar3 black but opens at/above bar2's close -> no downside gap.
|
||||
t.update(c(12.5, 13.0, 9.9, 10.0, 2));
|
||||
assert_eq!(t.update(c(14.0, 14.1, 8.9, 9.0, 3)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn third_bar_no_upper_shadow_yields_zero() {
|
||||
let mut t = ConcealingBabySwallow::new();
|
||||
t.update(c(20.0, 20.1, 14.9, 15.0, 0));
|
||||
t.update(c(16.0, 16.1, 11.9, 12.0, 1));
|
||||
// bar3 black, gaps down, but its high does not reach into bar2's body.
|
||||
t.update(c(11.0, 11.5, 9.9, 10.0, 2));
|
||||
assert_eq!(t.update(c(14.0, 14.1, 8.9, 9.0, 3)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fourth_bar_not_black_yields_zero() {
|
||||
let mut t = ConcealingBabySwallow::new();
|
||||
t.update(c(20.0, 20.1, 14.9, 15.0, 0));
|
||||
t.update(c(16.0, 16.1, 11.9, 12.0, 1));
|
||||
t.update(c(11.0, 13.0, 9.9, 10.0, 2));
|
||||
// bar4 white.
|
||||
assert_eq!(t.update(c(14.0, 14.1, 8.9, 14.05, 3)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fourth_bar_not_engulfing_yields_zero() {
|
||||
let mut t = ConcealingBabySwallow::new();
|
||||
t.update(c(20.0, 20.1, 14.9, 15.0, 0));
|
||||
t.update(c(16.0, 16.1, 11.9, 12.0, 1));
|
||||
t.update(c(11.0, 13.0, 9.9, 10.0, 2));
|
||||
// bar4 black but does not engulf bar3's high.
|
||||
assert_eq!(t.update(c(12.5, 12.6, 8.9, 9.0, 3)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 200.0 - i as f64;
|
||||
c(base, base + 0.05, base - 5.0, base - 5.0, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = ConcealingBabySwallow::new();
|
||||
let mut b = ConcealingBabySwallow::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = ConcealingBabySwallow::new();
|
||||
t.update(c(20.0, 20.1, 14.9, 15.0, 0));
|
||||
t.update(c(16.0, 16.1, 11.9, 12.0, 1));
|
||||
t.update(c(11.0, 13.0, 9.9, 10.0, 2));
|
||||
t.update(c(14.0, 14.1, 8.9, 9.0, 3));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 0)), Some(0.0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
//! Counterattack candlestick pattern.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Counterattack — a 2-bar reversal where the second bar storms back to close
|
||||
/// right where the first bar closed. A long candle runs with the trend, then an
|
||||
/// opposite-coloured long candle opens far in the trend direction and rallies (or
|
||||
/// sells off) all the way back to the prior close — the two closes meeting forms
|
||||
/// the "counterattack line".
|
||||
///
|
||||
/// ```text
|
||||
/// long bodies = |close − open| >= 0.5 * (high − low) (both bars)
|
||||
/// equal closes = |close2 − close1| <= tol * mean(range1, range2)
|
||||
/// bullish (+1.0): bar1 black (down), bar2 white (up), equal closes
|
||||
/// bearish (−1.0): bar1 white (up), bar2 black (down), equal closes
|
||||
/// ```
|
||||
///
|
||||
/// Output is `+1.0` bullish, `−1.0` bearish, and `0.0` when the bodies are short,
|
||||
/// the colours match, or the closes are not level. The first bar always returns
|
||||
/// `0.0` because the two-bar window is not yet filled. `equal_tolerance` defaults
|
||||
/// to `0.05` (TA-Lib's `CDLCOUNTERATTACK` "equal" factor — 5 % of the mean bar
|
||||
/// range) and must lie in `[0, 1)`. The body-length test uses a fixed half-range
|
||||
/// fraction rather than TA-Lib's rolling body average, matching the geometric
|
||||
/// house style of this pattern family. Pattern-shape check only — no trend filter
|
||||
/// is applied; combine with a trend indicator for actionable signals.
|
||||
///
|
||||
/// # Signed ±1 encoding
|
||||
///
|
||||
/// This detector emits the uniform candlestick sign convention shared across the
|
||||
/// pattern family — `+1.0` bullish, `−1.0` bearish, `0.0` no pattern — so it
|
||||
/// drops straight into a machine-learning feature matrix where the bullish and
|
||||
/// bearish variants occupy a single dimension.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Counterattack, Indicator};
|
||||
///
|
||||
/// let mut indicator = Counterattack::new();
|
||||
/// // Bullish: a long black bar, then a long white bar closing at the same level.
|
||||
/// indicator.update(Candle::new(20.0, 20.1, 14.9, 15.0, 1.0, 0).unwrap());
|
||||
/// let out = indicator
|
||||
/// .update(Candle::new(10.0, 15.1, 9.9, 15.0, 1.0, 1).unwrap());
|
||||
/// assert_eq!(out, Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Counterattack {
|
||||
equal_tolerance: f64,
|
||||
prev: Option<Candle>,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl Default for Counterattack {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Counterattack {
|
||||
/// Construct a Counterattack detector with the default 5 % equal-close tolerance.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
equal_tolerance: 0.05,
|
||||
prev: None,
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a Counterattack detector with a custom equal-close tolerance.
|
||||
///
|
||||
/// `equal_tolerance` is the fraction of the mean bar range within which the
|
||||
/// two closes must agree and must lie in `[0, 1)`.
|
||||
pub fn with_tolerance(equal_tolerance: f64) -> Result<Self> {
|
||||
if !(0.0..1.0).contains(&equal_tolerance) {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "counterattack equal tolerance must lie in [0, 1)",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
equal_tolerance,
|
||||
prev: None,
|
||||
has_emitted: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured equal-close tolerance.
|
||||
pub fn equal_tolerance(&self) -> f64 {
|
||||
self.equal_tolerance
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for Counterattack {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let prev = self.prev;
|
||||
self.prev = Some(candle);
|
||||
let Some(bar1) = prev else {
|
||||
return Some(0.0);
|
||||
};
|
||||
let range1 = bar1.high - bar1.low;
|
||||
let range2 = candle.high - candle.low;
|
||||
let body1 = bar1.close - bar1.open;
|
||||
let body2 = candle.close - candle.open;
|
||||
let long1 = body1.abs() >= 0.5 * range1;
|
||||
let long2 = body2.abs() >= 0.5 * range2;
|
||||
let tol = self.equal_tolerance * 0.5 * (range1 + range2);
|
||||
let equal_close = (candle.close - bar1.close).abs() <= tol;
|
||||
if !(long1 && long2 && equal_close) {
|
||||
return Some(0.0);
|
||||
}
|
||||
// Bullish: a long black bar met by a long white bar closing level.
|
||||
if body1 < 0.0 && body2 > 0.0 {
|
||||
return Some(1.0);
|
||||
}
|
||||
// Bearish: a long white bar met by a long black bar closing level.
|
||||
if body1 > 0.0 && body2 < 0.0 {
|
||||
return Some(-1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.prev = None;
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
2
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"Counterattack"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||
Candle::new(open, high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_tolerance() {
|
||||
assert!(Counterattack::with_tolerance(-0.01).is_err());
|
||||
assert!(Counterattack::with_tolerance(1.0).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_valid_tolerance() {
|
||||
let t = Counterattack::with_tolerance(0.0).unwrap();
|
||||
assert!((t.equal_tolerance() - 0.0).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let t = Counterattack::default();
|
||||
assert_eq!(t.name(), "Counterattack");
|
||||
assert_eq!(t.warmup_period(), 2);
|
||||
assert!(!t.is_ready());
|
||||
assert!((t.equal_tolerance() - 0.05).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bullish_counterattack_is_plus_one() {
|
||||
let mut t = Counterattack::new();
|
||||
assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 1)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bearish_counterattack_is_minus_one() {
|
||||
let mut t = Counterattack::new();
|
||||
assert_eq!(t.update(c(15.0, 20.1, 14.9, 20.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(25.0, 25.1, 19.9, 20.0, 1)), Some(-1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unequal_close_yields_zero() {
|
||||
let mut t = Counterattack::new();
|
||||
t.update(c(20.0, 20.1, 14.9, 15.0, 0));
|
||||
// Second close at 17.0 is far from the first close (15.0) -> not level.
|
||||
assert_eq!(t.update(c(10.0, 17.1, 9.9, 17.0, 1)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_color_yields_zero() {
|
||||
let mut t = Counterattack::new();
|
||||
// Both bars black -> not opposite colours.
|
||||
t.update(c(20.0, 20.1, 14.9, 15.0, 0));
|
||||
assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 1)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_body_yields_zero() {
|
||||
let mut t = Counterattack::new();
|
||||
// Second bar has a tiny body relative to its range.
|
||||
t.update(c(20.0, 20.1, 14.9, 15.0, 0));
|
||||
assert_eq!(t.update(c(14.8, 20.0, 9.9, 15.2, 1)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_bar_returns_zero() {
|
||||
let mut t = Counterattack::new();
|
||||
assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 100.0 + i as f64;
|
||||
c(base, base + 2.0, base - 2.0, base + 1.5, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = Counterattack::new();
|
||||
let mut b = Counterattack::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = Counterattack::new();
|
||||
t.update(c(20.0, 20.1, 14.9, 15.0, 0));
|
||||
t.update(c(10.0, 15.1, 9.9, 15.0, 1));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 0)), Some(0.0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
//! Cumulative Volume Delta — running sum of signed trade volume.
|
||||
|
||||
use crate::microstructure::Trade;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Cumulative Volume Delta (CVD) — the running sum of [signed volume].
|
||||
///
|
||||
/// ```text
|
||||
/// CVDₜ = CVDₜ₋₁ + sizeₜ · (+1 if buy, −1 if sell)
|
||||
/// ```
|
||||
///
|
||||
/// CVD is an unbounded running total: a rising line signals net buying pressure
|
||||
/// over the session, a falling line net selling. Divergence between CVD and
|
||||
/// price is a classic absorption / exhaustion signal. Call [`reset`] at the
|
||||
/// start of each session to re-anchor the cumulative total at zero.
|
||||
///
|
||||
/// `Input = Trade`, `Output = f64`. Ready after the first trade.
|
||||
///
|
||||
/// [signed volume]: crate::SignedVolume
|
||||
/// [`reset`]: crate::Indicator::reset
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{CumulativeVolumeDelta, Indicator, Side, Trade};
|
||||
///
|
||||
/// let mut cvd = CumulativeVolumeDelta::new();
|
||||
/// assert_eq!(cvd.update(Trade::new(100.0, 5.0, Side::Buy, 0).unwrap()), Some(5.0));
|
||||
/// assert_eq!(cvd.update(Trade::new(100.0, 2.0, Side::Sell, 1).unwrap()), Some(3.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct CumulativeVolumeDelta {
|
||||
cumulative: f64,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl CumulativeVolumeDelta {
|
||||
/// Construct a new CVD indicator with a zero running total.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
cumulative: 0.0,
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for CumulativeVolumeDelta {
|
||||
type Input = Trade;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, trade: Trade) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
self.cumulative += trade.size * trade.side.sign();
|
||||
Some(self.cumulative)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.cumulative = 0.0;
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"CumulativeVolumeDelta"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::microstructure::Side;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn trade(size: f64, side: Side, ts: i64) -> Trade {
|
||||
Trade::new(100.0, size, side, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let cvd = CumulativeVolumeDelta::new();
|
||||
assert_eq!(cvd.name(), "CumulativeVolumeDelta");
|
||||
assert_eq!(cvd.warmup_period(), 1);
|
||||
assert!(!cvd.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accumulates_signed_volume() {
|
||||
let mut cvd = CumulativeVolumeDelta::new();
|
||||
assert_eq!(cvd.update(trade(5.0, Side::Buy, 0)), Some(5.0));
|
||||
assert_eq!(cvd.update(trade(2.0, Side::Sell, 1)), Some(3.0));
|
||||
assert_eq!(cvd.update(trade(4.0, Side::Sell, 2)), Some(-1.0));
|
||||
assert!(cvd.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let trades: Vec<Trade> = (0..20)
|
||||
.map(|i| {
|
||||
let side = if i % 3 == 0 { Side::Sell } else { Side::Buy };
|
||||
trade(1.0 + (i % 4) as f64, side, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = CumulativeVolumeDelta::new();
|
||||
let mut b = CumulativeVolumeDelta::new();
|
||||
assert_eq!(
|
||||
a.batch(&trades),
|
||||
trades.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_re_anchors_at_zero() {
|
||||
let mut cvd = CumulativeVolumeDelta::new();
|
||||
cvd.update(trade(5.0, Side::Buy, 0));
|
||||
cvd.reset();
|
||||
assert!(!cvd.is_ready());
|
||||
// After reset the running total starts again from zero.
|
||||
assert_eq!(cvd.update(trade(2.0, Side::Buy, 1)), Some(2.0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
//! Depth Slope — how fast resting liquidity accumulates away from the mid.
|
||||
|
||||
use crate::microstructure::{Level, OrderBook};
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Ordinary-least-squares slope of cumulative resting size against distance
|
||||
/// from the mid, over the levels of one book side.
|
||||
///
|
||||
/// `signed_distance` is `+1.0` for the ask side (price above the mid) and
|
||||
/// `−1.0` for the bid side (price below the mid), so the regressor `x` —
|
||||
/// distance from the mid — is non-negative on both sides. The response `y` is
|
||||
/// the cumulative size walking outward from the touch. Returns `0.0` for a
|
||||
/// degenerate fit where every level sits at the same distance (zero variance in
|
||||
/// `x`).
|
||||
fn cumulative_slope(levels: &[Level], mid: f64, signed_distance: f64) -> f64 {
|
||||
let count = levels.len() as f64;
|
||||
let mut cumulative = 0.0;
|
||||
let mut sum_x = 0.0;
|
||||
let mut sum_y = 0.0;
|
||||
let mut sum_xy = 0.0;
|
||||
let mut sum_xx = 0.0;
|
||||
for level in levels {
|
||||
let x = signed_distance * (level.price - mid);
|
||||
cumulative += level.size;
|
||||
sum_x += x;
|
||||
sum_y += cumulative;
|
||||
sum_xy += x * cumulative;
|
||||
sum_xx += x * x;
|
||||
}
|
||||
let denom = count * sum_xx - sum_x * sum_x;
|
||||
if denom == 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
(count * sum_xy - sum_x * sum_y) / denom
|
||||
}
|
||||
|
||||
/// Depth Slope — the average rate at which cumulative resting size grows with
|
||||
/// distance from the mid, across the bid and ask sides of the book.
|
||||
///
|
||||
/// For each side the indicator runs an ordinary-least-squares regression of
|
||||
/// cumulative size (walking outward from the touch) on the level's distance
|
||||
/// from the mid, then reports the mean of the two slopes:
|
||||
///
|
||||
/// ```text
|
||||
/// slope_side = OLS slope of (|priceᵢ − mid|, Σ_{j≤i} sizeⱼ)
|
||||
/// depthSlope = (slope_bid + slope_ask) / 2
|
||||
/// ```
|
||||
///
|
||||
/// Because the response is *cumulative* size it never decreases with distance,
|
||||
/// so the slope is non-negative: it is a magnitude, not a direction. A large
|
||||
/// slope means cumulative liquidity builds quickly away from the touch — a deep
|
||||
/// book that absorbs large orders with little walking; a small slope is a thin,
|
||||
/// shallow book. A book whose size is concentrated at the touch and thins out
|
||||
/// behind it (a fragile book) reads a *smaller* slope than one of equal total
|
||||
/// depth that thickens with distance.
|
||||
///
|
||||
/// A side with fewer than two levels carries no slope, so the indicator returns
|
||||
/// `0.0` whenever either side has fewer than two levels (including an empty
|
||||
/// book).
|
||||
///
|
||||
/// `Input = OrderBook`, `Output = f64`. Stateless; ready after the first
|
||||
/// snapshot.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{DepthSlope, Indicator, Level, OrderBook};
|
||||
///
|
||||
/// // Both sides thicken linearly away from the mid (sizes 1, 2, 3 …).
|
||||
/// let book = OrderBook::new(
|
||||
/// vec![Level::new(99.0, 1.0).unwrap(), Level::new(98.0, 2.0).unwrap()],
|
||||
/// vec![Level::new(101.0, 1.0).unwrap(), Level::new(102.0, 2.0).unwrap()],
|
||||
/// )
|
||||
/// .unwrap();
|
||||
/// let mut ds = DepthSlope::new();
|
||||
/// assert!(ds.update(book).unwrap() > 0.0);
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct DepthSlope {
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl DepthSlope {
|
||||
/// Construct a new depth-slope indicator.
|
||||
pub const fn new() -> Self {
|
||||
Self { has_emitted: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for DepthSlope {
|
||||
type Input = OrderBook;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, book: OrderBook) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let Some(mid) = book.mid() else {
|
||||
return Some(0.0);
|
||||
};
|
||||
if book.bids.len() < 2 || book.asks.len() < 2 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let bid_slope = cumulative_slope(&book.bids, mid, -1.0);
|
||||
let ask_slope = cumulative_slope(&book.asks, mid, 1.0);
|
||||
Some(f64::midpoint(bid_slope, ask_slope))
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"DepthSlope"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn book(bids: &[(f64, f64)], asks: &[(f64, f64)]) -> OrderBook {
|
||||
let to_levels = |xs: &[(f64, f64)]| {
|
||||
xs.iter()
|
||||
.map(|&(p, s)| Level::new(p, s).unwrap())
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
OrderBook::new(to_levels(bids), to_levels(asks)).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let ds = DepthSlope::new();
|
||||
assert_eq!(ds.name(), "DepthSlope");
|
||||
assert_eq!(ds.warmup_period(), 1);
|
||||
assert!(!ds.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thickening_book_has_positive_slope() {
|
||||
let mut ds = DepthSlope::new();
|
||||
let out = ds
|
||||
.update(book(
|
||||
&[(99.0, 1.0), (98.0, 2.0), (97.0, 3.0)],
|
||||
&[(101.0, 1.0), (102.0, 2.0), (103.0, 3.0)],
|
||||
))
|
||||
.unwrap();
|
||||
assert!(out > 0.0);
|
||||
assert!(ds.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn front_loaded_book_has_smaller_slope_than_back_loaded() {
|
||||
// Same total depth (6 per side), but one book thickens away from the
|
||||
// touch and the other thins. Cumulative slope is non-negative for both;
|
||||
// the back-loaded book accumulates faster, so its slope is larger.
|
||||
let mut back = DepthSlope::new();
|
||||
let back_slope = back
|
||||
.update(book(
|
||||
&[(99.0, 1.0), (98.0, 2.0), (97.0, 3.0)],
|
||||
&[(101.0, 1.0), (102.0, 2.0), (103.0, 3.0)],
|
||||
))
|
||||
.unwrap();
|
||||
let mut front = DepthSlope::new();
|
||||
let front_slope = front
|
||||
.update(book(
|
||||
&[(99.0, 3.0), (98.0, 2.0), (97.0, 1.0)],
|
||||
&[(101.0, 3.0), (102.0, 2.0), (103.0, 1.0)],
|
||||
))
|
||||
.unwrap();
|
||||
assert!(front_slope >= 0.0);
|
||||
assert!(back_slope > front_slope);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_slope_value() {
|
||||
// Symmetric book, each side: distances 1, 2; cumulative sizes 1, 3.
|
||||
// OLS slope of (1->1, 2->3) = 2. Mean of two equal sides = 2.
|
||||
let mut ds = DepthSlope::new();
|
||||
let out = ds
|
||||
.update(book(
|
||||
&[(99.0, 1.0), (98.0, 2.0)],
|
||||
&[(101.0, 1.0), (102.0, 2.0)],
|
||||
))
|
||||
.unwrap();
|
||||
assert!((out - 2.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_level_side_is_zero() {
|
||||
let mut ds = DepthSlope::new();
|
||||
// Bid side has only one level -> no slope -> 0.
|
||||
assert_eq!(
|
||||
ds.update(book(&[(100.0, 1.0)], &[(101.0, 1.0), (102.0, 1.0)])),
|
||||
Some(0.0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_book_is_zero() {
|
||||
let mut ds = DepthSlope::new();
|
||||
assert_eq!(
|
||||
ds.update(OrderBook::new_unchecked(vec![], vec![])),
|
||||
Some(0.0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn degenerate_distance_slope_is_zero() {
|
||||
// Two levels at the same distance from mid carry zero x-variance.
|
||||
let levels = [
|
||||
Level::new_unchecked(100.0, 1.0),
|
||||
Level::new_unchecked(100.0, 2.0),
|
||||
];
|
||||
assert_eq!(cumulative_slope(&levels, 100.0, 1.0), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let books: Vec<OrderBook> = (0..20)
|
||||
.map(|i| {
|
||||
let extra = f64::from(i % 4);
|
||||
book(
|
||||
&[(99.0, 1.0 + extra), (98.0, 2.0)],
|
||||
&[(101.0, 1.0), (102.0, 2.0 + extra)],
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let mut a = DepthSlope::new();
|
||||
let mut b = DepthSlope::new();
|
||||
assert_eq!(
|
||||
a.batch(&books),
|
||||
books
|
||||
.iter()
|
||||
.map(|x| b.update(x.clone()))
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut ds = DepthSlope::new();
|
||||
ds.update(book(
|
||||
&[(99.0, 1.0), (98.0, 2.0)],
|
||||
&[(101.0, 1.0), (102.0, 2.0)],
|
||||
));
|
||||
assert!(ds.is_ready());
|
||||
ds.reset();
|
||||
assert!(!ds.is_ready());
|
||||
}
|
||||
}
|
||||
@@ -16,22 +16,44 @@ use crate::traits::Indicator;
|
||||
/// doji = body <= body_threshold * range
|
||||
/// ```
|
||||
///
|
||||
/// The output is `+1.0` when a Doji is detected and `0.0` otherwise. Doji is
|
||||
/// directionless — no `−1.0` is emitted. Pattern-shape check only — no trend
|
||||
/// filter is applied; combine with a trend indicator for actionable signals.
|
||||
/// # Signed ±1 encoding
|
||||
///
|
||||
/// By default the output is `+1.0` when a Doji is detected and `0.0`
|
||||
/// otherwise — a direction-less detection flag. For a drop-in machine-learning
|
||||
/// feature where every candlestick pattern shares the same sign convention
|
||||
/// (`+1.0` bullish, `−1.0` bearish, `0.0` none), switch the detector into
|
||||
/// signed mode with [`Doji::signed`]. A detected Doji is then classified by
|
||||
/// where its (negligible) body sits within the bar's range:
|
||||
///
|
||||
/// ```text
|
||||
/// pos = (0.5 * (open + close) − low) / (high − low)
|
||||
/// pos > 2/3 -> +1.0 dragonfly (long lower shadow, bullish)
|
||||
/// pos < 1/3 -> −1.0 gravestone (long upper shadow, bearish)
|
||||
/// else -> 0.0 long-legged / standard (neutral)
|
||||
/// ```
|
||||
///
|
||||
/// Pattern-shape check only — no trend filter is applied; combine with a trend
|
||||
/// indicator for actionable signals.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Doji, Indicator};
|
||||
///
|
||||
/// // Default: direction-less detection flag.
|
||||
/// let mut indicator = Doji::default();
|
||||
/// let candle = Candle::new(10.0, 11.0, 9.0, 10.0, 1.0, 0).unwrap();
|
||||
/// assert_eq!(indicator.update(candle), Some(1.0));
|
||||
///
|
||||
/// // Signed: a dragonfly Doji (body at the top, long lower shadow) is bullish.
|
||||
/// let mut signed = Doji::new().signed();
|
||||
/// let dragonfly = Candle::new(10.0, 10.05, 6.0, 10.0, 1.0, 0).unwrap();
|
||||
/// assert_eq!(signed.update(dragonfly), Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Doji {
|
||||
body_threshold: f64,
|
||||
signed: bool,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
@@ -46,6 +68,7 @@ impl Doji {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
body_threshold: 0.1,
|
||||
signed: false,
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
@@ -61,14 +84,32 @@ impl Doji {
|
||||
}
|
||||
Ok(Self {
|
||||
body_threshold,
|
||||
signed: false,
|
||||
has_emitted: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Switch to the signed dragonfly / gravestone encoding (consuming builder).
|
||||
///
|
||||
/// In signed mode a detected Doji emits `+1.0` (dragonfly, bullish),
|
||||
/// `−1.0` (gravestone, bearish) or `0.0` (long-legged / neutral) instead of
|
||||
/// the default direction-less `+1.0` detection flag. See the type-level
|
||||
/// docs for the exact classification rule.
|
||||
#[must_use]
|
||||
pub fn signed(mut self) -> Self {
|
||||
self.signed = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Configured body / range threshold.
|
||||
pub fn body_threshold(&self) -> f64 {
|
||||
self.body_threshold
|
||||
}
|
||||
|
||||
/// Whether this detector emits the signed dragonfly / gravestone encoding.
|
||||
pub fn is_signed(&self) -> bool {
|
||||
self.signed
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for Doji {
|
||||
@@ -82,11 +123,23 @@ impl Indicator for Doji {
|
||||
return Some(0.0);
|
||||
}
|
||||
let body = (candle.close - candle.open).abs();
|
||||
Some(if body <= self.body_threshold * range {
|
||||
1.0
|
||||
if body > self.body_threshold * range {
|
||||
return Some(0.0);
|
||||
}
|
||||
if !self.signed {
|
||||
return Some(1.0);
|
||||
}
|
||||
// Signed mode: classify the Doji by where its (negligible) body sits
|
||||
// within the high–low range.
|
||||
let body_mid = 0.5 * (candle.open + candle.close);
|
||||
let pos = (body_mid - candle.low) / range;
|
||||
if pos > 2.0 / 3.0 {
|
||||
Some(1.0)
|
||||
} else if pos < 1.0 / 3.0 {
|
||||
Some(-1.0)
|
||||
} else {
|
||||
0.0
|
||||
})
|
||||
Some(0.0)
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
@@ -134,6 +187,7 @@ mod tests {
|
||||
assert_eq!(d.name(), "Doji");
|
||||
assert_eq!(d.warmup_period(), 1);
|
||||
assert!(!d.is_ready());
|
||||
assert!(!d.is_signed());
|
||||
assert!((d.body_threshold() - 0.1).abs() < 1e-12);
|
||||
}
|
||||
|
||||
@@ -182,4 +236,81 @@ mod tests {
|
||||
d.reset();
|
||||
assert!(!d.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signed_accessor_and_builder() {
|
||||
let d = Doji::new().signed();
|
||||
assert!(d.is_signed());
|
||||
// The consuming builder composes with `with_threshold`.
|
||||
let t = Doji::with_threshold(0.05).unwrap().signed();
|
||||
assert!(t.is_signed());
|
||||
assert!((t.body_threshold() - 0.05).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signed_dragonfly_is_plus_one() {
|
||||
// Body at the top of the range, long lower shadow -> bullish.
|
||||
let mut d = Doji::new().signed();
|
||||
assert_eq!(d.update(c(10.0, 10.05, 6.0, 10.0, 0)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signed_gravestone_is_minus_one() {
|
||||
// Body at the bottom of the range, long upper shadow -> bearish.
|
||||
let mut d = Doji::new().signed();
|
||||
assert_eq!(d.update(c(10.0, 14.0, 9.95, 10.0, 0)), Some(-1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signed_long_legged_is_zero() {
|
||||
// Body centred, symmetric shadows -> neutral.
|
||||
let mut d = Doji::new().signed();
|
||||
assert_eq!(d.update(c(10.0, 12.0, 8.0, 10.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signed_non_doji_is_zero() {
|
||||
// A large body is not a Doji at all -> 0 regardless of position.
|
||||
let mut d = Doji::new().signed();
|
||||
assert_eq!(d.update(c(10.0, 12.0, 10.0, 12.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signed_zero_range_is_zero() {
|
||||
let mut d = Doji::new().signed();
|
||||
assert_eq!(d.update(c(10.0, 10.0, 10.0, 10.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signed_batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 100.0 + i as f64;
|
||||
// Alternate dragonfly / gravestone / centred Doji shapes.
|
||||
match i % 3 {
|
||||
0 => c(base, base + 0.05, base - 4.0, base, i),
|
||||
1 => c(base, base + 4.0, base - 0.05, base, i),
|
||||
_ => c(base, base + 2.0, base - 2.0, base, i),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let mut a = Doji::new().signed();
|
||||
let mut b = Doji::new().signed();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signed_survives_reset() {
|
||||
let mut d = Doji::new().signed();
|
||||
d.update(c(10.0, 10.05, 6.0, 10.0, 0));
|
||||
assert!(d.is_ready());
|
||||
d.reset();
|
||||
assert!(!d.is_ready());
|
||||
// `reset` clears only the streaming state, not the signed configuration.
|
||||
assert!(d.is_signed());
|
||||
assert_eq!(d.update(c(10.0, 10.05, 6.0, 10.0, 1)), Some(1.0));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
//! Doji Star candlestick pattern.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Doji Star — a 2-bar reversal warning. A long trending body is followed by a
|
||||
/// doji whose tiny body gaps away in the direction of the trend, the indecision
|
||||
/// hinting the move is about to turn.
|
||||
///
|
||||
/// ```text
|
||||
/// long body = |close − open| >= 0.5 * (high − low) (bar1)
|
||||
/// doji = |close − open| <= 0.1 * (high − low) (bar2)
|
||||
/// bullish (+1.0): bar1 black, doji body gaps DOWN below it (max(o2,c2) < close1)
|
||||
/// bearish (−1.0): bar1 white, doji body gaps UP above it (min(o2,c2) > close1)
|
||||
/// ```
|
||||
///
|
||||
/// Output is `+1.0` (bullish star, after a black bar) or `−1.0` (bearish star,
|
||||
/// after a white bar) when the pattern completes, and `0.0` otherwise. The first
|
||||
/// bar always returns `0.0` because the two-bar window is not yet filled. Doji
|
||||
/// thresholds follow the geometric house style (fixed half-range body for the
|
||||
/// long bar, tenth-range body for the doji) rather than TA-Lib's rolling
|
||||
/// averages. Pattern-shape check only — no trend filter is applied; combine with
|
||||
/// a trend indicator for actionable signals.
|
||||
///
|
||||
/// # Signed ±1 encoding
|
||||
///
|
||||
/// This detector emits the uniform candlestick sign convention shared across the
|
||||
/// pattern family — `+1.0` bullish, `−1.0` bearish, `0.0` no pattern — so it
|
||||
/// drops straight into a machine-learning feature matrix where the bullish and
|
||||
/// bearish variants occupy a single dimension.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, DojiStar, Indicator};
|
||||
///
|
||||
/// let mut indicator = DojiStar::new();
|
||||
/// // Long black bar, then a doji gapping down -> bullish star.
|
||||
/// indicator.update(Candle::new(20.0, 20.2, 14.8, 15.0, 1.0, 0).unwrap());
|
||||
/// let out = indicator
|
||||
/// .update(Candle::new(13.0, 13.1, 12.9, 13.0, 1.0, 1).unwrap());
|
||||
/// assert_eq!(out, Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct DojiStar {
|
||||
prev: Option<Candle>,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl DojiStar {
|
||||
/// Construct a new Doji Star detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
prev: None,
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for DojiStar {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let prev = self.prev;
|
||||
self.prev = Some(candle);
|
||||
let Some(bar1) = prev else {
|
||||
return Some(0.0);
|
||||
};
|
||||
let range1 = bar1.high - bar1.low;
|
||||
let range2 = candle.high - candle.low;
|
||||
if range1 <= 0.0 || range2 <= 0.0 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let body1 = bar1.close - bar1.open;
|
||||
if body1.abs() < 0.5 * range1 {
|
||||
return Some(0.0);
|
||||
}
|
||||
if (candle.close - candle.open).abs() > 0.1 * range2 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let doji_top = candle.open.max(candle.close);
|
||||
let doji_bottom = candle.open.min(candle.close);
|
||||
// Bullish: long black bar, doji body gaps down below it.
|
||||
if body1 < 0.0 && doji_top < bar1.close {
|
||||
return Some(1.0);
|
||||
}
|
||||
// Bearish: long white bar, doji body gaps up above it.
|
||||
if body1 > 0.0 && doji_bottom > bar1.close {
|
||||
return Some(-1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.prev = None;
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
2
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"DojiStar"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||
Candle::new(open, high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let t = DojiStar::new();
|
||||
assert_eq!(t.name(), "DojiStar");
|
||||
assert_eq!(t.warmup_period(), 2);
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bullish_doji_star_is_plus_one() {
|
||||
let mut t = DojiStar::new();
|
||||
assert_eq!(t.update(c(20.0, 20.2, 14.8, 15.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(13.0, 13.1, 12.9, 13.0, 1)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bearish_doji_star_is_minus_one() {
|
||||
let mut t = DojiStar::new();
|
||||
assert_eq!(t.update(c(15.0, 20.2, 14.8, 20.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(22.0, 22.1, 21.9, 22.0, 1)), Some(-1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn second_bar_not_doji_yields_zero() {
|
||||
let mut t = DojiStar::new();
|
||||
t.update(c(20.0, 20.2, 14.8, 15.0, 0));
|
||||
// Wide body, not a doji.
|
||||
assert_eq!(t.update(c(13.0, 13.2, 11.0, 11.5, 1)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_gap_yields_zero() {
|
||||
let mut t = DojiStar::new();
|
||||
t.update(c(20.0, 20.2, 14.8, 15.0, 0));
|
||||
// Doji overlaps bar1's body (no gap down).
|
||||
assert_eq!(t.update(c(16.0, 16.1, 15.9, 16.0, 1)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_first_body_yields_zero() {
|
||||
let mut t = DojiStar::new();
|
||||
// First bar body too short to be the "long" leg.
|
||||
t.update(c(20.0, 24.0, 16.0, 19.5, 0));
|
||||
assert_eq!(t.update(c(13.0, 13.1, 12.9, 13.0, 1)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_bar_returns_zero() {
|
||||
let mut t = DojiStar::new();
|
||||
assert_eq!(t.update(c(20.0, 20.2, 14.8, 15.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 100.0 + i as f64;
|
||||
if i % 2 == 0 {
|
||||
c(base + 5.0, base + 5.2, base - 0.2, base, i)
|
||||
} else {
|
||||
c(base - 3.0, base - 2.9, base - 3.1, base - 3.0, i)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let mut a = DojiStar::new();
|
||||
let mut b = DojiStar::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = DojiStar::new();
|
||||
t.update(c(20.0, 20.2, 14.8, 15.0, 0));
|
||||
t.update(c(13.0, 13.1, 12.9, 13.0, 1));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.update(c(20.0, 20.2, 14.8, 15.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_range_yields_zero() {
|
||||
let mut t = DojiStar::new();
|
||||
t.update(c(20.0, 20.2, 14.8, 15.0, 0));
|
||||
// Flat second bar (high == low) -> zero-range guard.
|
||||
assert_eq!(t.update(c(13.0, 13.0, 13.0, 13.0, 1)), Some(0.0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
//! Downside Gap Three Methods candlestick pattern.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Downside Gap Three Methods — a 3-bar bearish continuation. Two black candles
|
||||
/// decline with a downside body gap between them, then a white candle opens inside
|
||||
/// the second body and closes inside the first body, partially filling the gap
|
||||
/// without erasing the prior decline.
|
||||
///
|
||||
/// ```text
|
||||
/// bar1 black, bar2 black
|
||||
/// downside body gap: open2 < close1 (bar2's body sits entirely below bar1's)
|
||||
/// bar3 white, opens within bar2's body and closes within bar1's body
|
||||
/// ```
|
||||
///
|
||||
/// Output is `−1.0` when the pattern completes and `0.0` otherwise. Downside Gap
|
||||
/// Three Methods is a single-direction (bearish-only) continuation, so it never
|
||||
/// emits `+1.0`; its bullish mirror is [`crate::UpsideGapThreeMethods`]. The first
|
||||
/// two bars always return `0.0` because the three-bar window is not yet filled.
|
||||
/// Pattern-shape check only — no trend filter is applied; combine with a trend
|
||||
/// indicator for actionable signals.
|
||||
///
|
||||
/// # Signed ±1 encoding
|
||||
///
|
||||
/// This detector emits the uniform candlestick sign convention shared across the
|
||||
/// pattern family — `−1.0` bearish, `0.0` no pattern — so it drops straight into
|
||||
/// a machine-learning feature matrix as a single dimension.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, DownsideGapThreeMethods, Indicator};
|
||||
///
|
||||
/// let mut indicator = DownsideGapThreeMethods::new();
|
||||
/// indicator.update(Candle::new(13.0, 13.2, 11.8, 12.0, 1.0, 0).unwrap());
|
||||
/// indicator.update(Candle::new(11.0, 11.1, 9.8, 10.0, 1.0, 1).unwrap());
|
||||
/// let out = indicator
|
||||
/// .update(Candle::new(10.5, 12.6, 10.4, 12.5, 1.0, 2).unwrap());
|
||||
/// assert_eq!(out, Some(-1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct DownsideGapThreeMethods {
|
||||
c1: Option<Candle>,
|
||||
c2: Option<Candle>,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl DownsideGapThreeMethods {
|
||||
/// Construct a new Downside Gap Three Methods detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
c1: None,
|
||||
c2: None,
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for DownsideGapThreeMethods {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let bar1 = self.c1;
|
||||
let bar2 = self.c2;
|
||||
self.c1 = self.c2;
|
||||
self.c2 = Some(candle);
|
||||
let (Some(bar1), Some(bar2)) = (bar1, bar2) else {
|
||||
return Some(0.0);
|
||||
};
|
||||
// bar1 and bar2 are both black.
|
||||
if bar1.close >= bar1.open || bar2.close >= bar2.open {
|
||||
return Some(0.0);
|
||||
}
|
||||
// Downside body gap: bar2's body sits entirely below bar1's.
|
||||
if bar2.open >= bar1.close {
|
||||
return Some(0.0);
|
||||
}
|
||||
// bar3 is white.
|
||||
if candle.close <= candle.open {
|
||||
return Some(0.0);
|
||||
}
|
||||
// bar3 opens within bar2's body and closes within bar1's body.
|
||||
if candle.open > bar2.close
|
||||
&& candle.open < bar2.open
|
||||
&& candle.close > bar1.close
|
||||
&& candle.close < bar1.open
|
||||
{
|
||||
return Some(-1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.c1 = None;
|
||||
self.c2 = None;
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
3
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"DownsideGapThreeMethods"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||
Candle::new(open, high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let t = DownsideGapThreeMethods::new();
|
||||
assert_eq!(t.name(), "DownsideGapThreeMethods");
|
||||
assert_eq!(t.warmup_period(), 3);
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn downside_gap_three_methods_is_minus_one() {
|
||||
let mut t = DownsideGapThreeMethods::new();
|
||||
assert_eq!(t.update(c(13.0, 13.2, 11.8, 12.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(11.0, 11.1, 9.8, 10.0, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(10.5, 12.6, 10.4, 12.5, 2)), Some(-1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_two_bars_return_zero() {
|
||||
let mut t = DownsideGapThreeMethods::new();
|
||||
assert_eq!(t.update(c(13.0, 13.2, 11.8, 12.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(11.0, 11.1, 9.8, 10.0, 1)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_black_first_bars_yield_zero() {
|
||||
let mut t = DownsideGapThreeMethods::new();
|
||||
// bar1 is white.
|
||||
t.update(c(11.0, 13.2, 10.8, 13.0, 0));
|
||||
t.update(c(11.0, 11.1, 9.8, 10.0, 1));
|
||||
assert_eq!(t.update(c(10.5, 12.6, 10.4, 12.5, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_gap_yields_zero() {
|
||||
let mut t = DownsideGapThreeMethods::new();
|
||||
t.update(c(13.0, 13.2, 11.8, 12.0, 0));
|
||||
// bar2 opens above bar1's close -> no downside body gap.
|
||||
t.update(c(12.5, 12.6, 11.4, 11.5, 1));
|
||||
assert_eq!(t.update(c(11.5, 12.6, 11.4, 12.0, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn third_bar_not_white_yields_zero() {
|
||||
let mut t = DownsideGapThreeMethods::new();
|
||||
t.update(c(13.0, 13.2, 11.8, 12.0, 0));
|
||||
t.update(c(11.0, 11.1, 9.8, 10.0, 1));
|
||||
// bar3 black.
|
||||
assert_eq!(t.update(c(12.5, 12.6, 10.4, 10.5, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn third_bar_outside_bodies_yields_zero() {
|
||||
let mut t = DownsideGapThreeMethods::new();
|
||||
t.update(c(13.0, 13.2, 11.8, 12.0, 0));
|
||||
t.update(c(11.0, 11.1, 9.8, 10.0, 1));
|
||||
// bar3 white but closes above bar1's body.
|
||||
assert_eq!(t.update(c(10.5, 14.0, 10.4, 13.5, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 200.0 - i as f64;
|
||||
c(base, base + 0.1, base - 5.2, base - 5.0, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = DownsideGapThreeMethods::new();
|
||||
let mut b = DownsideGapThreeMethods::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = DownsideGapThreeMethods::new();
|
||||
t.update(c(13.0, 13.2, 11.8, 12.0, 0));
|
||||
t.update(c(11.0, 11.1, 9.8, 10.0, 1));
|
||||
t.update(c(10.5, 12.6, 10.4, 12.5, 2));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.update(c(13.0, 13.2, 11.8, 12.0, 0)), Some(0.0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
//! Dragonfly Doji candlestick pattern.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Dragonfly Doji — a single-bar bullish reversal. Open, close, and high sit at
|
||||
/// the top of the bar while a long lower shadow shows price was driven down hard
|
||||
/// and then bid all the way back to the open — buyers rejecting the lows.
|
||||
///
|
||||
/// ```text
|
||||
/// range = high − low
|
||||
/// doji = |close − open| <= 0.1 * range
|
||||
/// no upper wick = high − max(open, close) <= 0.1 * range
|
||||
/// long lower = min(open, close) − low >= 0.5 * range
|
||||
/// ```
|
||||
///
|
||||
/// Output is `+1.0` when the dragonfly prints and `0.0` otherwise. Dragonfly Doji
|
||||
/// is a single-direction (bullish-only) shape, so it never emits `−1.0`. Body and
|
||||
/// shadow thresholds follow the geometric house style (fixed fractions of the bar
|
||||
/// range) rather than TA-Lib's rolling averages. Pattern-shape check only — no
|
||||
/// trend filter is applied; combine with a trend indicator for actionable
|
||||
/// signals.
|
||||
///
|
||||
/// # Signed ±1 encoding
|
||||
///
|
||||
/// This detector emits the uniform candlestick sign convention shared across the
|
||||
/// pattern family — `+1.0` bullish, `0.0` no pattern — so it drops straight into
|
||||
/// a machine-learning feature matrix as a single dimension.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, DragonflyDoji, Indicator};
|
||||
///
|
||||
/// let mut indicator = DragonflyDoji::new();
|
||||
/// // Body at the top, long lower shadow.
|
||||
/// let candle = Candle::new(10.0, 10.05, 6.0, 10.0, 1.0, 0).unwrap();
|
||||
/// assert_eq!(indicator.update(candle), Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct DragonflyDoji {
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl DragonflyDoji {
|
||||
/// Construct a new Dragonfly Doji detector.
|
||||
pub const fn new() -> Self {
|
||||
Self { has_emitted: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for DragonflyDoji {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let range = candle.high - candle.low;
|
||||
if range <= 0.0 {
|
||||
return Some(0.0);
|
||||
}
|
||||
if (candle.close - candle.open).abs() > 0.1 * range {
|
||||
return Some(0.0);
|
||||
}
|
||||
let upper = candle.high - candle.open.max(candle.close);
|
||||
let lower = candle.open.min(candle.close) - candle.low;
|
||||
if upper <= 0.1 * range && lower >= 0.5 * range {
|
||||
return Some(1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"DragonflyDoji"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||
Candle::new(open, high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let t = DragonflyDoji::new();
|
||||
assert_eq!(t.name(), "DragonflyDoji");
|
||||
assert_eq!(t.warmup_period(), 1);
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dragonfly_is_plus_one() {
|
||||
let mut t = DragonflyDoji::new();
|
||||
assert_eq!(t.update(c(10.0, 10.05, 6.0, 10.0, 0)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upper_shadow_yields_zero() {
|
||||
let mut t = DragonflyDoji::new();
|
||||
// Long upper shadow -> not a dragonfly (this is a gravestone shape).
|
||||
assert_eq!(t.update(c(10.0, 14.0, 9.95, 10.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_lower_shadow_yields_zero() {
|
||||
let mut t = DragonflyDoji::new();
|
||||
// Body at the top but the lower shadow is too short.
|
||||
assert_eq!(t.update(c(10.0, 10.05, 9.6, 10.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_doji_yields_zero() {
|
||||
let mut t = DragonflyDoji::new();
|
||||
assert_eq!(t.update(c(10.0, 12.0, 6.0, 11.5, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_range_yields_zero() {
|
||||
let mut t = DragonflyDoji::new();
|
||||
assert_eq!(t.update(c(10.0, 10.0, 10.0, 10.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 100.0 + i as f64;
|
||||
c(base, base + 0.05, base - 4.0, base, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = DragonflyDoji::new();
|
||||
let mut b = DragonflyDoji::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = DragonflyDoji::new();
|
||||
t.update(c(10.0, 10.05, 6.0, 10.0, 0));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
//! Effective Spread — the realised cost of a single trade in basis points.
|
||||
|
||||
use crate::microstructure::TradeQuote;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Effective Spread — twice the signed deviation of an executed trade price
|
||||
/// from the prevailing mid, expressed in basis points of the mid.
|
||||
///
|
||||
/// ```text
|
||||
/// effectiveSpread = 2 · D · (tradePrice − mid) / mid · 10_000 (bps)
|
||||
/// ```
|
||||
///
|
||||
/// where `D` is the aggressor sign (`+1` for a buy, `−1` for a sell). The
|
||||
/// factor of two scales the one-sided deviation up to a full round-trip cost so
|
||||
/// it is directly comparable to the [quoted spread]: a marketable order that
|
||||
/// fills exactly at the touch of an otherwise quoted-spread book pays an
|
||||
/// effective spread equal to the quoted spread. Trades that fill *inside* the
|
||||
/// spread (price improvement) read below the quoted spread; trades that walk
|
||||
/// the book read above it.
|
||||
///
|
||||
/// A buy printed above the mid (`tradePrice > mid`) and a sell printed below it
|
||||
/// both yield a positive effective spread — the conventional sign, since the
|
||||
/// aggressor pays in both cases. A trade printed on the wrong side of the mid
|
||||
/// for its aggressor flag (a buy below the mid) reads negative, the signature of
|
||||
/// price improvement or a stale/mislabelled quote.
|
||||
///
|
||||
/// `Input = TradeQuote`, `Output = f64`. Stateless; ready after the first
|
||||
/// trade-quote.
|
||||
///
|
||||
/// [quoted spread]: crate::QuotedSpread
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{EffectiveSpread, Indicator, Side, Trade, TradeQuote};
|
||||
///
|
||||
/// let mut es = EffectiveSpread::new();
|
||||
/// // Buy filled at 100.05 against a mid of 100.0:
|
||||
/// // 2 · (+1) · (100.05 − 100.0) / 100.0 · 10_000 = 10 bps.
|
||||
/// let trade = Trade::new(100.05, 1.0, Side::Buy, 0).unwrap();
|
||||
/// let quote = TradeQuote::new(trade, 100.0).unwrap();
|
||||
/// assert!((es.update(quote).unwrap() - 10.0).abs() < 1e-9);
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct EffectiveSpread {
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl EffectiveSpread {
|
||||
/// Construct a new effective-spread indicator.
|
||||
pub const fn new() -> Self {
|
||||
Self { has_emitted: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for EffectiveSpread {
|
||||
type Input = TradeQuote;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, quote: TradeQuote) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let sign = quote.trade.side.sign();
|
||||
Some(2.0 * sign * (quote.trade.price - quote.mid) / quote.mid * 10_000.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"EffectiveSpread"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::microstructure::{Side, Trade};
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn quote(price: f64, side: Side, mid: f64) -> TradeQuote {
|
||||
TradeQuote::new(Trade::new(price, 1.0, side, 0).unwrap(), mid).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let es = EffectiveSpread::new();
|
||||
assert_eq!(es.name(), "EffectiveSpread");
|
||||
assert_eq!(es.warmup_period(), 1);
|
||||
assert!(!es.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buy_above_mid_is_positive() {
|
||||
let mut es = EffectiveSpread::new();
|
||||
// 2 · (+1) · (100.05 − 100.0) / 100.0 · 10_000 = 10 bps.
|
||||
let out = es.update(quote(100.05, Side::Buy, 100.0)).unwrap();
|
||||
assert!((out - 10.0).abs() < 1e-9);
|
||||
assert!(es.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sell_below_mid_is_positive() {
|
||||
let mut es = EffectiveSpread::new();
|
||||
// 2 · (−1) · (99.95 − 100.0) / 100.0 · 10_000 = 10 bps.
|
||||
let out = es.update(quote(99.95, Side::Sell, 100.0)).unwrap();
|
||||
assert!((out - 10.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn price_improvement_reads_negative() {
|
||||
let mut es = EffectiveSpread::new();
|
||||
// A buy filled below the mid: price improvement -> negative.
|
||||
let out = es.update(quote(99.95, Side::Buy, 100.0)).unwrap();
|
||||
assert!(out < 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trade_at_mid_is_zero() {
|
||||
let mut es = EffectiveSpread::new();
|
||||
assert_eq!(es.update(quote(100.0, Side::Buy, 100.0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let quotes: Vec<TradeQuote> = (0..20)
|
||||
.map(|i| {
|
||||
let side = if i % 2 == 0 { Side::Buy } else { Side::Sell };
|
||||
let price = 100.0 + f64::from(i % 4) * 0.01;
|
||||
quote(price, side, 100.0)
|
||||
})
|
||||
.collect();
|
||||
let mut a = EffectiveSpread::new();
|
||||
let mut b = EffectiveSpread::new();
|
||||
assert_eq!(
|
||||
a.batch("es),
|
||||
quotes.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut es = EffectiveSpread::new();
|
||||
es.update(quote(100.05, Side::Buy, 100.0));
|
||||
assert!(es.is_ready());
|
||||
es.reset();
|
||||
assert!(!es.is_ready());
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,13 @@ use crate::traits::Indicator;
|
||||
/// body exists to engulf. Pattern-shape check only — no trend filter is
|
||||
/// applied; combine with a trend indicator for actionable signals.
|
||||
///
|
||||
/// # Signed ±1 encoding
|
||||
///
|
||||
/// This detector already emits the uniform candlestick sign convention shared
|
||||
/// across the pattern family — `+1.0` bullish, `−1.0` bearish, `0.0` no
|
||||
/// pattern — so it drops straight into a machine-learning feature matrix where
|
||||
/// the bullish and bearish variants of the pattern occupy a single dimension.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
//! Evening Doji Star candlestick pattern.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Evening Doji Star — a 3-bar bearish top reversal. A long white bar extends the
|
||||
/// advance, a doji gaps up above it (the star of indecision), then a black bar
|
||||
/// gaps back down and closes deep into the first body, confirming the turn.
|
||||
///
|
||||
/// ```text
|
||||
/// long body = |close − open| >= 0.5 * (high − low)
|
||||
/// doji = |close − open| <= 0.1 * (high − low)
|
||||
/// bar1 white & long
|
||||
/// bar2 doji, body gaps UP above bar1 body (min(o2,c2) > close1)
|
||||
/// bar3 black, body gaps DOWN below the doji (max(o3,c3) < min(o2,c2))
|
||||
/// bar3 closes deep into bar1 body (close3 < close1 − penetration·body1)
|
||||
/// ```
|
||||
///
|
||||
/// Output is `−1.0` when the pattern completes and `0.0` otherwise. Evening Doji
|
||||
/// Star is a single-direction (bearish-only) reversal, so it never emits `+1.0`.
|
||||
/// The first two bars always return `0.0` because the three-bar window is not yet
|
||||
/// filled. `penetration` is how far into the first body the third bar must close;
|
||||
/// it defaults to `0.3` (TA-Lib's `CDLEVENINGDOJISTAR` default) and must lie in
|
||||
/// `[0, 1)`. Body and doji thresholds follow the geometric house style rather than
|
||||
/// TA-Lib's rolling averages. Pattern-shape check only — no trend filter is
|
||||
/// applied; combine with a trend indicator for actionable signals.
|
||||
///
|
||||
/// # Signed ±1 encoding
|
||||
///
|
||||
/// This detector emits the uniform candlestick sign convention shared across the
|
||||
/// pattern family — `−1.0` bearish, `0.0` no pattern — so it drops straight into
|
||||
/// a machine-learning feature matrix as a single dimension.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, EveningDojiStar, Indicator};
|
||||
///
|
||||
/// let mut indicator = EveningDojiStar::new();
|
||||
/// indicator.update(Candle::new(10.0, 15.1, 9.9, 15.0, 1.0, 0).unwrap());
|
||||
/// indicator.update(Candle::new(17.0, 17.1, 16.9, 17.0, 1.0, 1).unwrap());
|
||||
/// let out = indicator
|
||||
/// .update(Candle::new(16.0, 16.1, 11.9, 12.0, 1.0, 2).unwrap());
|
||||
/// assert_eq!(out, Some(-1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EveningDojiStar {
|
||||
penetration: f64,
|
||||
prev: Option<Candle>,
|
||||
prev_prev: Option<Candle>,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl Default for EveningDojiStar {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl EveningDojiStar {
|
||||
/// Construct an Evening Doji Star detector with the default 0.3 penetration.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
penetration: 0.3,
|
||||
prev: None,
|
||||
prev_prev: None,
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct an Evening Doji Star detector with a custom penetration fraction.
|
||||
///
|
||||
/// `penetration` must lie in `[0, 1)`.
|
||||
pub fn with_penetration(penetration: f64) -> Result<Self> {
|
||||
if !(0.0..1.0).contains(&penetration) {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "evening doji star penetration must lie in [0, 1)",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
penetration,
|
||||
prev: None,
|
||||
prev_prev: None,
|
||||
has_emitted: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured penetration fraction.
|
||||
pub fn penetration(&self) -> f64 {
|
||||
self.penetration
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for EveningDojiStar {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let bar1 = self.prev_prev;
|
||||
let bar2 = self.prev;
|
||||
self.prev_prev = self.prev;
|
||||
self.prev = Some(candle);
|
||||
let (Some(bar1), Some(bar2)) = (bar1, bar2) else {
|
||||
return Some(0.0);
|
||||
};
|
||||
let range1 = bar1.high - bar1.low;
|
||||
let range2 = bar2.high - bar2.low;
|
||||
if range1 <= 0.0 || range2 <= 0.0 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let body1 = bar1.close - bar1.open;
|
||||
if body1 < 0.5 * range1 {
|
||||
return Some(0.0); // bar1 must be a long white body
|
||||
}
|
||||
if (bar2.close - bar2.open).abs() > 0.1 * range2 {
|
||||
return Some(0.0); // bar2 must be a doji
|
||||
}
|
||||
let star_bottom = bar2.open.min(bar2.close);
|
||||
let bar3_top = candle.open.max(candle.close);
|
||||
if star_bottom > bar1.close
|
||||
&& candle.close < candle.open
|
||||
&& bar3_top < star_bottom
|
||||
&& candle.close < bar1.close - self.penetration * body1
|
||||
{
|
||||
return Some(-1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.prev = None;
|
||||
self.prev_prev = None;
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
3
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"EveningDojiStar"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||
Candle::new(open, high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_penetration() {
|
||||
assert!(EveningDojiStar::with_penetration(-0.01).is_err());
|
||||
assert!(EveningDojiStar::with_penetration(1.0).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_valid_penetration() {
|
||||
let t = EveningDojiStar::with_penetration(0.5).unwrap();
|
||||
assert!((t.penetration() - 0.5).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let t = EveningDojiStar::default();
|
||||
assert_eq!(t.name(), "EveningDojiStar");
|
||||
assert_eq!(t.warmup_period(), 3);
|
||||
assert!(!t.is_ready());
|
||||
assert!((t.penetration() - 0.3).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn evening_doji_star_is_minus_one() {
|
||||
let mut t = EveningDojiStar::new();
|
||||
assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(17.0, 17.1, 16.9, 17.0, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(16.0, 16.1, 11.9, 12.0, 2)), Some(-1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn middle_not_doji_yields_zero() {
|
||||
let mut t = EveningDojiStar::new();
|
||||
t.update(c(10.0, 15.1, 9.9, 15.0, 0));
|
||||
// Wide-bodied star, not a doji.
|
||||
t.update(c(16.0, 18.1, 15.9, 18.0, 1));
|
||||
assert_eq!(t.update(c(16.0, 16.1, 11.9, 12.0, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shallow_close_yields_zero() {
|
||||
let mut t = EveningDojiStar::new();
|
||||
t.update(c(10.0, 15.1, 9.9, 15.0, 0));
|
||||
t.update(c(17.0, 17.1, 16.9, 17.0, 1));
|
||||
// bar3 black but closes at 14.0 -> only 1.0 into the 5.0 body (< 0.3·5).
|
||||
assert_eq!(t.update(c(16.0, 16.1, 13.9, 14.0, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_two_bars_return_zero() {
|
||||
let mut t = EveningDojiStar::new();
|
||||
assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(17.0, 17.1, 16.9, 17.0, 1)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 100.0 + i as f64;
|
||||
c(base, base + 5.2, base - 0.1, base + 5.0, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = EveningDojiStar::new();
|
||||
let mut b = EveningDojiStar::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = EveningDojiStar::new();
|
||||
t.update(c(10.0, 15.1, 9.9, 15.0, 0));
|
||||
t.update(c(17.0, 17.1, 16.9, 17.0, 1));
|
||||
t.update(c(16.0, 16.1, 11.9, 12.0, 2));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_range_yields_zero() {
|
||||
let mut t = EveningDojiStar::new();
|
||||
// Flat first bar (range1 == 0) -> rejected.
|
||||
t.update(c(10.0, 10.0, 10.0, 10.0, 0));
|
||||
t.update(c(17.0, 17.1, 16.9, 17.0, 1));
|
||||
assert_eq!(t.update(c(16.0, 16.1, 11.9, 12.0, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_first_body_yields_zero() {
|
||||
let mut t = EveningDojiStar::new();
|
||||
// bar1 has a wide range but a tiny body -> not a long white body.
|
||||
t.update(c(10.0, 16.0, 9.0, 10.5, 0));
|
||||
t.update(c(17.0, 17.1, 16.9, 17.0, 1));
|
||||
assert_eq!(t.update(c(16.0, 16.1, 11.9, 12.0, 2)), Some(0.0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
//! Falling Three Methods candlestick pattern.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Falling Three Methods — a 5-bar bearish continuation. A long black candle is
|
||||
/// followed by three small bars that drift up but stay inside its range (a brief
|
||||
/// rest), then a second long black candle closes below the first, resuming the
|
||||
/// decline.
|
||||
///
|
||||
/// ```text
|
||||
/// long body = |close − open| >= 0.5 * (high − low)
|
||||
/// bar1 black & long
|
||||
/// bar2, bar3, bar4 small bodies, each contained within bar1's high/low range
|
||||
/// bar5 black, closing below bar1's close
|
||||
/// ```
|
||||
///
|
||||
/// Output is `−1.0` when the pattern completes and `0.0` otherwise. Falling Three
|
||||
/// Methods is a single-direction (bearish-only) continuation, so it never emits
|
||||
/// `+1.0`. The first four bars always return `0.0` because the five-bar window is
|
||||
/// not yet filled. Body thresholds follow the geometric house style rather than
|
||||
/// TA-Lib's rolling averages. Pattern-shape check only — no trend filter is
|
||||
/// applied; combine with a trend indicator for actionable signals.
|
||||
///
|
||||
/// # Signed ±1 encoding
|
||||
///
|
||||
/// This detector emits the uniform candlestick sign convention shared across the
|
||||
/// pattern family — `−1.0` bearish, `0.0` no pattern — so it drops straight into
|
||||
/// a machine-learning feature matrix as a single dimension.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, FallingThreeMethods, Indicator};
|
||||
///
|
||||
/// let mut indicator = FallingThreeMethods::new();
|
||||
/// indicator.update(Candle::new(15.0, 15.1, 9.9, 10.0, 1.0, 0).unwrap());
|
||||
/// indicator.update(Candle::new(11.0, 12.1, 10.9, 12.0, 1.0, 1).unwrap());
|
||||
/// indicator.update(Candle::new(11.5, 12.6, 11.4, 12.5, 1.0, 2).unwrap());
|
||||
/// indicator.update(Candle::new(12.0, 13.1, 11.9, 13.0, 1.0, 3).unwrap());
|
||||
/// let out = indicator
|
||||
/// .update(Candle::new(12.5, 12.6, 8.9, 9.0, 1.0, 4).unwrap());
|
||||
/// assert_eq!(out, Some(-1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct FallingThreeMethods {
|
||||
c1: Option<Candle>,
|
||||
c2: Option<Candle>,
|
||||
c3: Option<Candle>,
|
||||
c4: Option<Candle>,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl FallingThreeMethods {
|
||||
/// Construct a new Falling Three Methods detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
c1: None,
|
||||
c2: None,
|
||||
c3: None,
|
||||
c4: None,
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for FallingThreeMethods {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let bar1 = self.c1;
|
||||
let bar2 = self.c2;
|
||||
let bar3 = self.c3;
|
||||
let bar4 = self.c4;
|
||||
self.c1 = self.c2;
|
||||
self.c2 = self.c3;
|
||||
self.c3 = self.c4;
|
||||
self.c4 = Some(candle);
|
||||
let (Some(bar1), Some(bar2), Some(bar3), Some(bar4)) = (bar1, bar2, bar3, bar4) else {
|
||||
return Some(0.0);
|
||||
};
|
||||
let range1 = bar1.high - bar1.low;
|
||||
if range1 <= 0.0 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let body1 = bar1.open - bar1.close;
|
||||
if body1 < 0.5 * range1 {
|
||||
return Some(0.0); // bar1 must be a long black body
|
||||
}
|
||||
// The three middle bars stay within bar1's range with smaller bodies.
|
||||
for mid in [bar2, bar3, bar4] {
|
||||
if (mid.close - mid.open).abs() >= body1 || mid.high > bar1.high || mid.low < bar1.low {
|
||||
return Some(0.0);
|
||||
}
|
||||
}
|
||||
// bar5 is a black candle closing below bar1's close.
|
||||
if candle.close < candle.open && candle.close < bar1.close {
|
||||
return Some(-1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.c1 = None;
|
||||
self.c2 = None;
|
||||
self.c3 = None;
|
||||
self.c4 = None;
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
5
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"FallingThreeMethods"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||
Candle::new(open, high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let t = FallingThreeMethods::new();
|
||||
assert_eq!(t.name(), "FallingThreeMethods");
|
||||
assert_eq!(t.warmup_period(), 5);
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn falling_three_methods_is_minus_one() {
|
||||
let mut t = FallingThreeMethods::new();
|
||||
assert_eq!(t.update(c(15.0, 15.1, 9.9, 10.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(11.0, 12.1, 10.9, 12.0, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(11.5, 12.6, 11.4, 12.5, 2)), Some(0.0));
|
||||
assert_eq!(t.update(c(12.0, 13.1, 11.9, 13.0, 3)), Some(0.0));
|
||||
assert_eq!(t.update(c(12.5, 12.6, 8.9, 9.0, 4)), Some(-1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn middle_bar_breaks_range_yields_zero() {
|
||||
let mut t = FallingThreeMethods::new();
|
||||
t.update(c(15.0, 15.1, 9.9, 10.0, 0));
|
||||
t.update(c(11.0, 12.1, 10.9, 12.0, 1));
|
||||
// bar3 pokes below bar1's low.
|
||||
t.update(c(11.5, 12.6, 9.0, 12.5, 2));
|
||||
t.update(c(12.0, 13.1, 11.9, 13.0, 3));
|
||||
assert_eq!(t.update(c(12.5, 12.6, 8.9, 9.0, 4)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bar5_not_new_low_yields_zero() {
|
||||
let mut t = FallingThreeMethods::new();
|
||||
t.update(c(15.0, 15.1, 9.9, 10.0, 0));
|
||||
t.update(c(11.0, 12.1, 10.9, 12.0, 1));
|
||||
t.update(c(11.5, 12.6, 11.4, 12.5, 2));
|
||||
t.update(c(12.0, 13.1, 11.9, 13.0, 3));
|
||||
// bar5 black but closes above bar1's close.
|
||||
assert_eq!(t.update(c(12.5, 12.6, 10.4, 10.5, 4)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_four_bars_return_zero() {
|
||||
let mut t = FallingThreeMethods::new();
|
||||
assert_eq!(t.update(c(15.0, 15.1, 9.9, 10.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(11.0, 12.1, 10.9, 12.0, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(11.5, 12.6, 11.4, 12.5, 2)), Some(0.0));
|
||||
assert_eq!(t.update(c(12.0, 13.1, 11.9, 13.0, 3)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 200.0 - i as f64;
|
||||
c(base + 5.0, base + 5.1, base - 0.1, base, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = FallingThreeMethods::new();
|
||||
let mut b = FallingThreeMethods::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = FallingThreeMethods::new();
|
||||
t.update(c(15.0, 15.1, 9.9, 10.0, 0));
|
||||
t.update(c(11.0, 12.1, 10.9, 12.0, 1));
|
||||
t.update(c(11.5, 12.6, 11.4, 12.5, 2));
|
||||
t.update(c(12.0, 13.1, 11.9, 13.0, 3));
|
||||
t.update(c(12.5, 12.6, 8.9, 9.0, 4));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.update(c(15.0, 15.1, 9.9, 10.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_range_first_bar_yields_zero() {
|
||||
let mut t = FallingThreeMethods::new();
|
||||
// Flat first bar (range1 == 0) -> rejected.
|
||||
t.update(c(10.0, 10.0, 10.0, 10.0, 0));
|
||||
t.update(c(11.0, 12.1, 10.9, 12.0, 1));
|
||||
t.update(c(11.5, 12.6, 11.4, 12.5, 2));
|
||||
t.update(c(12.0, 13.1, 11.9, 13.0, 3));
|
||||
assert_eq!(t.update(c(12.5, 12.6, 8.9, 9.0, 4)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_first_body_yields_zero() {
|
||||
let mut t = FallingThreeMethods::new();
|
||||
// bar1 has a wide range but a tiny body -> not a long black body.
|
||||
t.update(c(10.0, 16.0, 9.0, 10.2, 0));
|
||||
t.update(c(11.0, 12.1, 10.9, 12.0, 1));
|
||||
t.update(c(11.5, 12.6, 11.4, 12.5, 2));
|
||||
t.update(c(12.0, 13.1, 11.9, 13.0, 3));
|
||||
assert_eq!(t.update(c(12.5, 12.6, 8.9, 9.0, 4)), Some(0.0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
//! Footprint — buy/sell volume profile per price bucket within a bar.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::microstructure::Trade;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// One price bucket of a [`Footprint`]: the buy- and sell-initiated volume that
|
||||
/// traded there since the last reset.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct FootprintLevel {
|
||||
/// Bucket price (the bucket index times the tick size).
|
||||
pub price: f64,
|
||||
/// Sell-initiated (bid-hitting) volume traded at this bucket.
|
||||
pub bid_vol: f64,
|
||||
/// Buy-initiated (ask-lifting) volume traded at this bucket.
|
||||
pub ask_vol: f64,
|
||||
}
|
||||
|
||||
/// The full footprint of a bar: one [`FootprintLevel`] per touched price
|
||||
/// bucket, sorted ascending by price.
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
pub struct FootprintOutput {
|
||||
/// Touched price buckets, lowest price first.
|
||||
pub levels: Vec<FootprintLevel>,
|
||||
}
|
||||
|
||||
/// Footprint — the buy/sell volume profile of a bar, bucketed by price.
|
||||
///
|
||||
/// A footprint (a.k.a. bid/ask or volume cluster chart) decomposes the volume
|
||||
/// traded within a bar across the price levels at which it printed, splitting
|
||||
/// each level into buy-initiated (ask-lifting) and sell-initiated (bid-hitting)
|
||||
/// volume. It exposes *where* inside a bar the activity happened and which side
|
||||
/// was the aggressor there — the basis for absorption, imbalance and
|
||||
/// point-of-control analysis that a single OHLCV bar hides.
|
||||
///
|
||||
/// Each trade is assigned to the price bucket `round(price / tick_size)`; its
|
||||
/// size is added to that bucket's ask volume for a buy and bid volume for a
|
||||
/// sell. Every [`update`] returns the complete footprint accumulated since the
|
||||
/// last [`reset`], as a [`FootprintOutput`] whose `levels` are sorted ascending
|
||||
/// by price. Call [`reset`] at each bar (or session) boundary to start a fresh
|
||||
/// footprint.
|
||||
///
|
||||
/// `Input = Trade`, `Output = FootprintOutput`. Ready after the first trade.
|
||||
///
|
||||
/// [`update`]: crate::Indicator::update
|
||||
/// [`reset`]: crate::Indicator::reset
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Footprint, Indicator, Side, Trade};
|
||||
///
|
||||
/// let mut fp = Footprint::new(1.0).unwrap();
|
||||
/// fp.update(Trade::new(100.2, 2.0, Side::Buy, 0).unwrap());
|
||||
/// let out = fp.update(Trade::new(100.7, 3.0, Side::Sell, 1).unwrap()).unwrap();
|
||||
/// // Two buckets: 100 (ask 2) and 101 (bid 3).
|
||||
/// assert_eq!(out.levels.len(), 2);
|
||||
/// assert_eq!(out.levels[0].price, 100.0);
|
||||
/// assert_eq!(out.levels[0].ask_vol, 2.0);
|
||||
/// assert_eq!(out.levels[1].price, 101.0);
|
||||
/// assert_eq!(out.levels[1].bid_vol, 3.0);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Footprint {
|
||||
tick_size: f64,
|
||||
// bucket index -> (bid_vol = sell-initiated, ask_vol = buy-initiated).
|
||||
buckets: BTreeMap<i64, (f64, f64)>,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl Footprint {
|
||||
/// Construct a footprint with the given price-bucket `tick_size`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::InvalidTick`] if `tick_size` is not a finite, strictly
|
||||
/// positive number.
|
||||
pub fn new(tick_size: f64) -> Result<Self> {
|
||||
if !tick_size.is_finite() || tick_size <= 0.0 {
|
||||
return Err(Error::InvalidTick {
|
||||
message: "footprint tick_size must be finite and positive",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
tick_size,
|
||||
buckets: BTreeMap::new(),
|
||||
has_emitted: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// The configured price-bucket size.
|
||||
pub const fn tick_size(&self) -> f64 {
|
||||
self.tick_size
|
||||
}
|
||||
|
||||
fn bucket_index(&self, price: f64) -> i64 {
|
||||
// Float-to-int `as` saturates rather than wrapping, so an extreme
|
||||
// price/tick ratio clamps to i64::MIN/MAX instead of misbehaving;
|
||||
// realistic ratios fit comfortably.
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
{
|
||||
(price / self.tick_size).round() as i64
|
||||
}
|
||||
}
|
||||
|
||||
fn snapshot(&self) -> FootprintOutput {
|
||||
let levels = self
|
||||
.buckets
|
||||
.iter()
|
||||
.map(|(&index, &(bid_vol, ask_vol))| FootprintLevel {
|
||||
price: index as f64 * self.tick_size,
|
||||
bid_vol,
|
||||
ask_vol,
|
||||
})
|
||||
.collect();
|
||||
FootprintOutput { levels }
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for Footprint {
|
||||
type Input = Trade;
|
||||
type Output = FootprintOutput;
|
||||
|
||||
fn update(&mut self, trade: Trade) -> Option<FootprintOutput> {
|
||||
self.has_emitted = true;
|
||||
let index = self.bucket_index(trade.price);
|
||||
let entry = self.buckets.entry(index).or_insert((0.0, 0.0));
|
||||
if trade.side.sign() > 0.0 {
|
||||
entry.1 += trade.size;
|
||||
} else {
|
||||
entry.0 += trade.size;
|
||||
}
|
||||
Some(self.snapshot())
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.buckets.clear();
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"Footprint"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::microstructure::Side;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn trade(price: f64, size: f64, side: Side) -> Trade {
|
||||
Trade::new(price, size, side, 0).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_bad_tick_size() {
|
||||
assert!(matches!(
|
||||
Footprint::new(0.0),
|
||||
Err(Error::InvalidTick { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
Footprint::new(-1.0),
|
||||
Err(Error::InvalidTick { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
Footprint::new(f64::NAN),
|
||||
Err(Error::InvalidTick { .. })
|
||||
));
|
||||
assert!(Footprint::new(0.5).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let fp = Footprint::new(0.25).unwrap();
|
||||
assert_eq!(fp.name(), "Footprint");
|
||||
assert_eq!(fp.warmup_period(), 1);
|
||||
assert_eq!(fp.tick_size(), 0.25);
|
||||
assert!(!fp.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buckets_buy_and_sell_volume() {
|
||||
let mut fp = Footprint::new(1.0).unwrap();
|
||||
fp.update(trade(100.2, 2.0, Side::Buy));
|
||||
fp.update(trade(100.7, 3.0, Side::Sell));
|
||||
let out = fp.update(trade(100.1, 1.0, Side::Buy)).unwrap();
|
||||
assert!(fp.is_ready());
|
||||
// Bucket 100: buy 2 + buy 1 = ask 3, bid 0. Bucket 101: sell 3.
|
||||
assert_eq!(out.levels.len(), 2);
|
||||
assert_eq!(out.levels[0].price, 100.0);
|
||||
assert_eq!(out.levels[0].ask_vol, 3.0);
|
||||
assert_eq!(out.levels[0].bid_vol, 0.0);
|
||||
assert_eq!(out.levels[1].price, 101.0);
|
||||
assert_eq!(out.levels[1].bid_vol, 3.0);
|
||||
assert_eq!(out.levels[1].ask_vol, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn levels_sorted_ascending_by_price() {
|
||||
let mut fp = Footprint::new(1.0).unwrap();
|
||||
fp.update(trade(103.0, 1.0, Side::Buy));
|
||||
fp.update(trade(100.0, 1.0, Side::Sell));
|
||||
let out = fp.update(trade(101.0, 1.0, Side::Buy)).unwrap();
|
||||
let prices: Vec<f64> = out.levels.iter().map(|l| l.price).collect();
|
||||
assert_eq!(prices, vec![100.0, 101.0, 103.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sub_tick_prices_share_a_bucket() {
|
||||
let mut fp = Footprint::new(0.5).unwrap();
|
||||
// 100.24 and 100.26 both round to bucket 200 (price 100.0)... check:
|
||||
// 100.24/0.5 = 200.48 -> 200; 100.26/0.5 = 200.52 -> 201. Distinct.
|
||||
fp.update(trade(100.20, 1.0, Side::Buy)); // 200.4 -> 200 -> price 100.0
|
||||
let out = fp.update(trade(100.10, 2.0, Side::Buy)).unwrap(); // 200.2 -> 200
|
||||
assert_eq!(out.levels.len(), 1);
|
||||
assert_eq!(out.levels[0].price, 100.0);
|
||||
assert_eq!(out.levels[0].ask_vol, 3.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_the_footprint() {
|
||||
let mut fp = Footprint::new(1.0).unwrap();
|
||||
fp.update(trade(100.0, 5.0, Side::Buy));
|
||||
assert!(fp.is_ready());
|
||||
fp.reset();
|
||||
assert!(!fp.is_ready());
|
||||
let out = fp.update(trade(200.0, 1.0, Side::Sell)).unwrap();
|
||||
assert_eq!(out.levels.len(), 1);
|
||||
assert_eq!(out.levels[0].price, 200.0);
|
||||
assert_eq!(out.levels[0].bid_vol, 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let trades: Vec<Trade> = (0..30)
|
||||
.map(|i| {
|
||||
let side = if i % 3 == 0 { Side::Sell } else { Side::Buy };
|
||||
trade(100.0 + f64::from(i % 5), 1.0 + f64::from(i % 4), side)
|
||||
})
|
||||
.collect();
|
||||
let mut a = Footprint::new(1.0).unwrap();
|
||||
let mut b = Footprint::new(1.0).unwrap();
|
||||
assert_eq!(
|
||||
a.batch(&trades),
|
||||
trades.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
//! Funding Basis — the perpetual mark's relative premium to the spot index.
|
||||
|
||||
use crate::derivatives::DerivativesTick;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Funding Basis — the relative basis between the perpetual mark price and the
|
||||
/// spot index it tracks.
|
||||
///
|
||||
/// ```text
|
||||
/// basis = (markPrice − indexPrice) / indexPrice
|
||||
/// ```
|
||||
///
|
||||
/// The basis is the spread that the funding mechanism continuously pulls toward
|
||||
/// zero: a positive basis (perpetual above spot) goes hand in hand with positive
|
||||
/// funding (longs pay), a negative basis with negative funding. Reading the
|
||||
/// instantaneous basis alongside the [funding rate] separates a genuine premium
|
||||
/// from a stale-funding artefact and sizes the carry available to a cash-and-carry
|
||||
/// or basis-arbitrage trade. The output is a fraction (e.g. `0.001` = 10 bps);
|
||||
/// multiply by `10_000` for basis points.
|
||||
///
|
||||
/// `Input = DerivativesTick`, `Output = f64`. Stateless; ready after the first
|
||||
/// tick.
|
||||
///
|
||||
/// [funding rate]: crate::FundingRate
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{DerivativesTick, FundingBasis, Indicator};
|
||||
///
|
||||
/// let mut fb = FundingBasis::new();
|
||||
/// // mark 100.5 vs index 100.0 -> (100.5 - 100.0) / 100.0 = 0.005.
|
||||
/// let tick = DerivativesTick::new(
|
||||
/// 0.0, 100.5, 100.0, 100.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0,
|
||||
/// )
|
||||
/// .unwrap();
|
||||
/// assert!((fb.update(tick).unwrap() - 0.005).abs() < 1e-12);
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct FundingBasis {
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl FundingBasis {
|
||||
/// Construct a new funding-basis indicator.
|
||||
#[must_use]
|
||||
pub const fn new() -> Self {
|
||||
Self { has_emitted: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for FundingBasis {
|
||||
type Input = DerivativesTick;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, tick: DerivativesTick) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
Some((tick.mark_price - tick.index_price) / tick.index_price)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"FundingBasis"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn tick(mark: f64, index: f64) -> DerivativesTick {
|
||||
DerivativesTick::new_unchecked(0.0, mark, index, mark, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let fb = FundingBasis::new();
|
||||
assert_eq!(fb.name(), "FundingBasis");
|
||||
assert_eq!(fb.warmup_period(), 1);
|
||||
assert!(!fb.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn premium_is_positive() {
|
||||
let mut fb = FundingBasis::new();
|
||||
let out = fb.update(tick(100.5, 100.0)).unwrap();
|
||||
assert!((out - 0.005).abs() < 1e-12);
|
||||
assert!(fb.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discount_is_negative() {
|
||||
let mut fb = FundingBasis::new();
|
||||
let out = fb.update(tick(99.5, 100.0)).unwrap();
|
||||
assert!((out + 0.005).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn at_par_is_zero() {
|
||||
let mut fb = FundingBasis::new();
|
||||
assert_eq!(fb.update(tick(100.0, 100.0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let ticks: Vec<DerivativesTick> = (0..20)
|
||||
.map(|i| tick(100.0 + f64::from(i % 5) * 0.1, 100.0))
|
||||
.collect();
|
||||
let mut a = FundingBasis::new();
|
||||
let mut b = FundingBasis::new();
|
||||
assert_eq!(
|
||||
a.batch(&ticks),
|
||||
ticks.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut fb = FundingBasis::new();
|
||||
fb.update(tick(100.5, 100.0));
|
||||
assert!(fb.is_ready());
|
||||
fb.reset();
|
||||
assert!(!fb.is_ready());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
//! Funding Rate — the current perpetual funding rate.
|
||||
|
||||
use crate::derivatives::DerivativesTick;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Funding Rate — the funding rate carried by each derivatives tick.
|
||||
///
|
||||
/// The funding rate is the periodic payment exchanged between long and short
|
||||
/// perpetual-swap holders that tethers the perpetual mark to the spot index. A
|
||||
/// positive rate means longs pay shorts (the perpetual trades at a premium); a
|
||||
/// negative rate means shorts pay longs (a discount). This indicator simply
|
||||
/// surfaces the rate from the [`DerivativesTick`] feed so it can be charted,
|
||||
/// chained or fed to the rolling funding statistics ([`FundingRateMean`],
|
||||
/// [`FundingRateZScore`]).
|
||||
///
|
||||
/// `Input = DerivativesTick`, `Output = f64`. Stateless; ready after the first
|
||||
/// tick.
|
||||
///
|
||||
/// [`FundingRateMean`]: crate::FundingRateMean
|
||||
/// [`FundingRateZScore`]: crate::FundingRateZScore
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{DerivativesTick, FundingRate, Indicator};
|
||||
///
|
||||
/// let mut fr = FundingRate::new();
|
||||
/// let tick = DerivativesTick::new(
|
||||
/// 0.0001, 100.0, 100.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0,
|
||||
/// )
|
||||
/// .unwrap();
|
||||
/// assert_eq!(fr.update(tick), Some(0.0001));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct FundingRate {
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl FundingRate {
|
||||
/// Construct a new funding-rate indicator.
|
||||
#[must_use]
|
||||
pub const fn new() -> Self {
|
||||
Self { has_emitted: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for FundingRate {
|
||||
type Input = DerivativesTick;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, tick: DerivativesTick) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
Some(tick.funding_rate)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"FundingRate"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn tick(funding_rate: f64) -> DerivativesTick {
|
||||
DerivativesTick::new_unchecked(
|
||||
funding_rate,
|
||||
100.0,
|
||||
100.0,
|
||||
100.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let fr = FundingRate::new();
|
||||
assert_eq!(fr.name(), "FundingRate");
|
||||
assert_eq!(fr.warmup_period(), 1);
|
||||
assert!(!fr.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn passes_through_funding_rate() {
|
||||
let mut fr = FundingRate::new();
|
||||
assert_eq!(fr.update(tick(0.0001)), Some(0.0001));
|
||||
assert_eq!(fr.update(tick(-0.0003)), Some(-0.0003));
|
||||
assert!(fr.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let ticks: Vec<DerivativesTick> =
|
||||
(0..20).map(|i| tick(0.0001 * f64::from(i - 10))).collect();
|
||||
let mut a = FundingRate::new();
|
||||
let mut b = FundingRate::new();
|
||||
assert_eq!(
|
||||
a.batch(&ticks),
|
||||
ticks.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut fr = FundingRate::new();
|
||||
fr.update(tick(0.0001));
|
||||
assert!(fr.is_ready());
|
||||
fr.reset();
|
||||
assert!(!fr.is_ready());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
//! Funding Rate Rolling Mean — average funding rate over a trailing window.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::derivatives::DerivativesTick;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Funding Rate Rolling Mean — the arithmetic mean of the funding rate over the
|
||||
/// trailing window of `window` ticks.
|
||||
///
|
||||
/// ```text
|
||||
/// mean = (1 / window) · Σ fundingRate over the last `window` ticks
|
||||
/// ```
|
||||
///
|
||||
/// Smoothing the raw [funding rate] reveals the persistent carry regime — a
|
||||
/// sustained positive mean marks a crowded-long market paying to hold the
|
||||
/// perpetual, a sustained negative mean a crowded-short one. The indicator warms
|
||||
/// up for `window` ticks — `update` returns `None` until the window is full —
|
||||
/// then emits the rolling mean, maintained in O(1) per tick via a running sum.
|
||||
///
|
||||
/// `Input = DerivativesTick`, `Output = f64`.
|
||||
///
|
||||
/// [funding rate]: crate::FundingRate
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{DerivativesTick, FundingRateMean, Indicator};
|
||||
///
|
||||
/// fn tick(rate: f64) -> DerivativesTick {
|
||||
/// DerivativesTick::new(rate, 100.0, 100.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0)
|
||||
/// .unwrap()
|
||||
/// }
|
||||
///
|
||||
/// let mut frm = FundingRateMean::new(2).unwrap();
|
||||
/// assert_eq!(frm.update(tick(0.001)), None);
|
||||
/// // Window full: (0.001 + 0.003) / 2 = 0.002.
|
||||
/// assert_eq!(frm.update(tick(0.003)), Some(0.002));
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FundingRateMean {
|
||||
window: usize,
|
||||
history: VecDeque<f64>,
|
||||
sum: f64,
|
||||
}
|
||||
|
||||
impl FundingRateMean {
|
||||
/// Construct a funding-rate rolling mean over a window of `window` ticks.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`] if `window` is zero.
|
||||
pub fn new(window: usize) -> Result<Self> {
|
||||
if window == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
Ok(Self {
|
||||
window,
|
||||
history: VecDeque::with_capacity(window),
|
||||
sum: 0.0,
|
||||
})
|
||||
}
|
||||
|
||||
/// The configured window length, in ticks.
|
||||
#[must_use]
|
||||
pub fn window(&self) -> usize {
|
||||
self.window
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for FundingRateMean {
|
||||
type Input = DerivativesTick;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, tick: DerivativesTick) -> Option<f64> {
|
||||
self.history.push_back(tick.funding_rate);
|
||||
self.sum += tick.funding_rate;
|
||||
if self.history.len() > self.window {
|
||||
let old = self.history.pop_front().expect("window >= 1, len > window");
|
||||
self.sum -= old;
|
||||
}
|
||||
if self.history.len() < self.window {
|
||||
return None;
|
||||
}
|
||||
Some(self.sum / self.window as f64)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.history.clear();
|
||||
self.sum = 0.0;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.window
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.history.len() >= self.window
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"FundingRateMean"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn tick(rate: f64) -> DerivativesTick {
|
||||
DerivativesTick::new_unchecked(
|
||||
rate, 100.0, 100.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_window() {
|
||||
assert!(matches!(FundingRateMean::new(0), Err(Error::PeriodZero)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let frm = FundingRateMean::new(5).unwrap();
|
||||
assert_eq!(frm.name(), "FundingRateMean");
|
||||
assert_eq!(frm.warmup_period(), 5);
|
||||
assert_eq!(frm.window(), 5);
|
||||
assert!(!frm.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warms_up_then_emits_mean() {
|
||||
let mut frm = FundingRateMean::new(2).unwrap();
|
||||
assert_eq!(frm.update(tick(0.001)), None);
|
||||
assert!(!frm.is_ready());
|
||||
assert_eq!(frm.update(tick(0.003)), Some(0.002));
|
||||
assert!(frm.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rolls_off_old_values() {
|
||||
let mut frm = FundingRateMean::new(2).unwrap();
|
||||
frm.update(tick(0.001));
|
||||
frm.update(tick(0.003)); // mean 0.002
|
||||
let out = frm.update(tick(0.005)).unwrap(); // window [0.003, 0.005] -> 0.004
|
||||
assert!((out - 0.004).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handles_negative_rates() {
|
||||
let mut frm = FundingRateMean::new(2).unwrap();
|
||||
frm.update(tick(-0.002));
|
||||
let out = frm.update(tick(0.004)).unwrap();
|
||||
assert!((out - 0.001).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let ticks: Vec<DerivativesTick> = (0..30)
|
||||
.map(|i| tick(0.0001 * f64::from(i % 7) - 0.0003))
|
||||
.collect();
|
||||
let mut a = FundingRateMean::new(5).unwrap();
|
||||
let mut b = FundingRateMean::new(5).unwrap();
|
||||
assert_eq!(
|
||||
a.batch(&ticks),
|
||||
ticks.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut frm = FundingRateMean::new(2).unwrap();
|
||||
frm.update(tick(0.001));
|
||||
frm.update(tick(0.003));
|
||||
assert!(frm.is_ready());
|
||||
frm.reset();
|
||||
assert!(!frm.is_ready());
|
||||
assert_eq!(frm.update(tick(0.002)), None);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
//! Funding Rate Z-Score — how extreme the latest funding rate is versus its
|
||||
//! recent history.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::derivatives::DerivativesTick;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Funding Rate Z-Score — the latest funding rate expressed in standard
|
||||
/// deviations from its rolling mean over the trailing window of `window` ticks.
|
||||
///
|
||||
/// ```text
|
||||
/// zScore = (fundingRate − mean) / population_stddev over the last `window` ticks
|
||||
/// ```
|
||||
///
|
||||
/// A reading of `+2` means funding is two standard deviations richer than its
|
||||
/// recent norm — an unusually crowded long, a contrarian fade signal; `−2` is
|
||||
/// the mirror. Normalising the [funding rate] this way makes funding extremes
|
||||
/// comparable across regimes and assets. A window with zero dispersion (a flat
|
||||
/// funding series) yields `0`. The indicator warms up for `window` ticks, then
|
||||
/// emits the rolling z-score, maintained in O(1) per tick.
|
||||
///
|
||||
/// `Input = DerivativesTick`, `Output = f64`.
|
||||
///
|
||||
/// [funding rate]: crate::FundingRate
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{DerivativesTick, FundingRateZScore, Indicator};
|
||||
///
|
||||
/// fn tick(rate: f64) -> DerivativesTick {
|
||||
/// DerivativesTick::new(rate, 100.0, 100.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0)
|
||||
/// .unwrap()
|
||||
/// }
|
||||
///
|
||||
/// let mut z = FundingRateZScore::new(2).unwrap();
|
||||
/// assert_eq!(z.update(tick(0.001)), None);
|
||||
/// // Window [0.001, 0.003]: mean 0.002, population stddev 0.001 -> (0.003 - 0.002) / 0.001 = 1.
|
||||
/// assert!((z.update(tick(0.003)).unwrap() - 1.0).abs() < 1e-9);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FundingRateZScore {
|
||||
window: usize,
|
||||
history: VecDeque<f64>,
|
||||
sum: f64,
|
||||
sum_sq: f64,
|
||||
}
|
||||
|
||||
impl FundingRateZScore {
|
||||
/// Construct a funding-rate z-score over a window of `window` ticks.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`] if `window` is zero.
|
||||
pub fn new(window: usize) -> Result<Self> {
|
||||
if window == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
Ok(Self {
|
||||
window,
|
||||
history: VecDeque::with_capacity(window),
|
||||
sum: 0.0,
|
||||
sum_sq: 0.0,
|
||||
})
|
||||
}
|
||||
|
||||
/// The configured window length, in ticks.
|
||||
#[must_use]
|
||||
pub fn window(&self) -> usize {
|
||||
self.window
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for FundingRateZScore {
|
||||
type Input = DerivativesTick;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, tick: DerivativesTick) -> Option<f64> {
|
||||
let value = tick.funding_rate;
|
||||
if self.history.len() == self.window {
|
||||
let old = self.history.pop_front().expect("non-empty");
|
||||
self.sum -= old;
|
||||
self.sum_sq -= old * old;
|
||||
}
|
||||
self.history.push_back(value);
|
||||
self.sum += value;
|
||||
self.sum_sq += value * value;
|
||||
if self.history.len() < self.window {
|
||||
return None;
|
||||
}
|
||||
let n = self.window as f64;
|
||||
let mean = self.sum / n;
|
||||
// Population variance E[x²] − E[x]²; clamp away tiny negative drift.
|
||||
let variance = (self.sum_sq / n - mean * mean).max(0.0);
|
||||
let std = variance.sqrt();
|
||||
if std == 0.0 {
|
||||
// A window with no dispersion: funding is exactly its own mean.
|
||||
return Some(0.0);
|
||||
}
|
||||
Some((value - mean) / std)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.history.clear();
|
||||
self.sum = 0.0;
|
||||
self.sum_sq = 0.0;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.window
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.history.len() == self.window
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"FundingRateZScore"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn tick(rate: f64) -> DerivativesTick {
|
||||
DerivativesTick::new_unchecked(
|
||||
rate, 100.0, 100.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_window() {
|
||||
assert!(matches!(FundingRateZScore::new(0), Err(Error::PeriodZero)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let z = FundingRateZScore::new(5).unwrap();
|
||||
assert_eq!(z.name(), "FundingRateZScore");
|
||||
assert_eq!(z.warmup_period(), 5);
|
||||
assert_eq!(z.window(), 5);
|
||||
assert!(!z.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_value() {
|
||||
let mut z = FundingRateZScore::new(2).unwrap();
|
||||
assert_eq!(z.update(tick(0.001)), None);
|
||||
// Window [0.001, 0.003]: mean 0.002, var (1e-6 + 9e-6)/2 - 4e-6 = 1e-6,
|
||||
// stddev 0.001; latest 0.003 is (0.003 - 0.002) / 0.001 = 1.
|
||||
let out = z.update(tick(0.003)).unwrap();
|
||||
assert!((out - 1.0).abs() < 1e-9);
|
||||
assert!(z.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flat_window_is_zero() {
|
||||
let mut z = FundingRateZScore::new(3).unwrap();
|
||||
z.update(tick(0.002));
|
||||
z.update(tick(0.002));
|
||||
assert_eq!(z.update(tick(0.002)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rolls_off_old_values() {
|
||||
let mut z = FundingRateZScore::new(2).unwrap();
|
||||
z.update(tick(0.001));
|
||||
z.update(tick(0.003));
|
||||
// Window now [0.003, 0.005]: mean 0.004, stddev 0.001 -> +1.
|
||||
let out = z.update(tick(0.005)).unwrap();
|
||||
assert!((out - 1.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let ticks: Vec<DerivativesTick> = (0..30)
|
||||
.map(|i| tick(0.0001 * f64::from(i % 5) - 0.0002))
|
||||
.collect();
|
||||
let mut a = FundingRateZScore::new(6).unwrap();
|
||||
let mut b = FundingRateZScore::new(6).unwrap();
|
||||
assert_eq!(
|
||||
a.batch(&ticks),
|
||||
ticks.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut z = FundingRateZScore::new(2).unwrap();
|
||||
z.update(tick(0.001));
|
||||
z.update(tick(0.003));
|
||||
assert!(z.is_ready());
|
||||
z.reset();
|
||||
assert!(!z.is_ready());
|
||||
assert_eq!(z.update(tick(0.002)), None);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
//! Gap Side-by-Side White Lines candlestick pattern.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Gap Side-by-Side White Lines — a 3-bar continuation. After a gap away from the
|
||||
/// first bar, two white candles of similar size open at roughly the same level
|
||||
/// (side by side) and hold the gap open, signalling the trend resumes in the gap
|
||||
/// direction.
|
||||
///
|
||||
/// ```text
|
||||
/// bar2, bar3 both white
|
||||
/// bar2 body gaps away from bar1 body (up or down)
|
||||
/// bar3 opens beside bar2 (|open3 − open2| <= 0.1 · range2)
|
||||
/// bar3 body is similar in size to bar2 (neither more than twice the other)
|
||||
/// gap up -> +1.0 (bullish continuation)
|
||||
/// gap down -> −1.0 (bearish continuation — "downside" gap side-by-side white)
|
||||
/// ```
|
||||
///
|
||||
/// Output is `+1.0` (gap up) or `−1.0` (gap down) when the pattern completes and
|
||||
/// `0.0` otherwise. The first two bars always return `0.0` because the three-bar
|
||||
/// window is not yet filled. Open-equality and body-similarity thresholds follow
|
||||
/// the geometric house style rather than TA-Lib's rolling averages. Pattern-shape
|
||||
/// check only — no trend filter is applied; combine with a trend indicator for
|
||||
/// actionable signals.
|
||||
///
|
||||
/// # Signed ±1 encoding
|
||||
///
|
||||
/// This detector emits the uniform candlestick sign convention shared across the
|
||||
/// pattern family — `+1.0` bullish, `−1.0` bearish, `0.0` no pattern — so it
|
||||
/// drops straight into a machine-learning feature matrix where the two gap
|
||||
/// directions occupy a single dimension.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, GapSideBySideWhite, Indicator};
|
||||
///
|
||||
/// let mut indicator = GapSideBySideWhite::new();
|
||||
/// indicator.update(Candle::new(10.0, 11.1, 9.9, 11.0, 1.0, 0).unwrap());
|
||||
/// indicator.update(Candle::new(13.0, 14.1, 12.9, 14.0, 1.0, 1).unwrap());
|
||||
/// let out = indicator
|
||||
/// .update(Candle::new(13.0, 14.1, 12.9, 14.0, 1.0, 2).unwrap());
|
||||
/// assert_eq!(out, Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct GapSideBySideWhite {
|
||||
prev: Option<Candle>,
|
||||
prev_prev: Option<Candle>,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl GapSideBySideWhite {
|
||||
/// Construct a new Gap Side-by-Side White Lines detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
prev: None,
|
||||
prev_prev: None,
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for GapSideBySideWhite {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let bar1 = self.prev_prev;
|
||||
let bar2 = self.prev;
|
||||
self.prev_prev = self.prev;
|
||||
self.prev = Some(candle);
|
||||
let (Some(bar1), Some(bar2)) = (bar1, bar2) else {
|
||||
return Some(0.0);
|
||||
};
|
||||
let range2 = bar2.high - bar2.low;
|
||||
if range2 <= 0.0 {
|
||||
return Some(0.0);
|
||||
}
|
||||
// Both of the side-by-side bars must be white.
|
||||
if bar2.close <= bar2.open || candle.close <= candle.open {
|
||||
return Some(0.0);
|
||||
}
|
||||
// Side by side: opens level and bodies of comparable size.
|
||||
if (candle.open - bar2.open).abs() > 0.1 * range2 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let body2 = bar2.close - bar2.open;
|
||||
let body3 = candle.close - candle.open;
|
||||
if body2 > 2.0 * body3 || body3 > 2.0 * body2 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let bar1_top = bar1.open.max(bar1.close);
|
||||
let bar1_bottom = bar1.open.min(bar1.close);
|
||||
let bar2_bottom = bar2.open.min(bar2.close);
|
||||
let bar2_top = bar2.open.max(bar2.close);
|
||||
if bar2_bottom > bar1_top {
|
||||
return Some(1.0); // gap up -> bullish continuation
|
||||
}
|
||||
if bar2_top < bar1_bottom {
|
||||
return Some(-1.0); // gap down -> bearish continuation
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.prev = None;
|
||||
self.prev_prev = None;
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
3
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"GapSideBySideWhite"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||
Candle::new(open, high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let t = GapSideBySideWhite::new();
|
||||
assert_eq!(t.name(), "GapSideBySideWhite");
|
||||
assert_eq!(t.warmup_period(), 3);
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gap_up_is_plus_one() {
|
||||
let mut t = GapSideBySideWhite::new();
|
||||
assert_eq!(t.update(c(10.0, 11.1, 9.9, 11.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(13.0, 14.1, 12.9, 14.0, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(13.0, 14.1, 12.9, 14.0, 2)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gap_down_is_minus_one() {
|
||||
let mut t = GapSideBySideWhite::new();
|
||||
assert_eq!(t.update(c(14.0, 14.1, 12.9, 13.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(10.0, 11.1, 9.9, 11.0, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(10.0, 11.1, 9.9, 11.0, 2)), Some(-1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn second_bar_black_yields_zero() {
|
||||
let mut t = GapSideBySideWhite::new();
|
||||
t.update(c(10.0, 11.1, 9.9, 11.0, 0));
|
||||
// bar3 is black -> not two white lines.
|
||||
t.update(c(13.0, 14.1, 12.9, 14.0, 1));
|
||||
assert_eq!(t.update(c(14.0, 14.1, 12.9, 13.0, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_side_by_side_yields_zero() {
|
||||
let mut t = GapSideBySideWhite::new();
|
||||
t.update(c(10.0, 11.1, 9.9, 11.0, 0));
|
||||
t.update(c(13.0, 14.1, 12.9, 14.0, 1));
|
||||
// bar3 opens far from bar2's open -> not side by side.
|
||||
assert_eq!(t.update(c(16.0, 17.1, 15.9, 17.0, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_gap_yields_zero() {
|
||||
let mut t = GapSideBySideWhite::new();
|
||||
t.update(c(10.0, 13.1, 9.9, 13.0, 0));
|
||||
// bar2 overlaps bar1 (no gap).
|
||||
t.update(c(12.0, 13.1, 11.9, 13.0, 1));
|
||||
assert_eq!(t.update(c(12.0, 13.1, 11.9, 13.0, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_two_bars_return_zero() {
|
||||
let mut t = GapSideBySideWhite::new();
|
||||
assert_eq!(t.update(c(10.0, 11.1, 9.9, 11.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(13.0, 14.1, 12.9, 14.0, 1)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 100.0 + i as f64 * 3.0;
|
||||
c(base, base + 1.1, base - 0.1, base + 1.0, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = GapSideBySideWhite::new();
|
||||
let mut b = GapSideBySideWhite::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = GapSideBySideWhite::new();
|
||||
t.update(c(10.0, 11.1, 9.9, 11.0, 0));
|
||||
t.update(c(13.0, 14.1, 12.9, 14.0, 1));
|
||||
t.update(c(13.0, 14.1, 12.9, 14.0, 2));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.update(c(10.0, 11.1, 9.9, 11.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_range_yields_zero() {
|
||||
let mut t = GapSideBySideWhite::new();
|
||||
t.update(c(10.0, 11.1, 9.9, 11.0, 0));
|
||||
// Flat second bar (range2 == 0) -> rejected.
|
||||
t.update(c(13.0, 13.0, 13.0, 13.0, 1));
|
||||
assert_eq!(t.update(c(13.0, 14.1, 12.9, 14.0, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn body_size_mismatch_yields_zero() {
|
||||
let mut t = GapSideBySideWhite::new();
|
||||
t.update(c(10.0, 11.1, 9.9, 11.0, 0));
|
||||
t.update(c(13.0, 16.0, 12.9, 15.0, 1)); // white, body 2.0
|
||||
// Level open, white, but its body is more than 2x smaller -> rejected.
|
||||
assert_eq!(t.update(c(13.0, 13.7, 12.9, 13.5, 2)), Some(0.0)); // body 0.5
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
//! Gravestone Doji candlestick pattern.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Gravestone Doji — a single-bar bearish reversal. Open, close, and low sit at
|
||||
/// the bottom of the bar while a long upper shadow shows price was pushed up hard
|
||||
/// and then sold all the way back to the open — sellers rejecting the highs.
|
||||
///
|
||||
/// ```text
|
||||
/// range = high − low
|
||||
/// doji = |close − open| <= 0.1 * range
|
||||
/// no lower wick = min(open, close) − low <= 0.1 * range
|
||||
/// long upper = high − max(open, close) >= 0.5 * range
|
||||
/// ```
|
||||
///
|
||||
/// Output is `−1.0` when the gravestone prints and `0.0` otherwise. Gravestone
|
||||
/// Doji is a single-direction (bearish-only) shape, so it never emits `+1.0`.
|
||||
/// Body and shadow thresholds follow the geometric house style (fixed fractions
|
||||
/// of the bar range) rather than TA-Lib's rolling averages. Pattern-shape check
|
||||
/// only — no trend filter is applied; combine with a trend indicator for
|
||||
/// actionable signals.
|
||||
///
|
||||
/// # Signed ±1 encoding
|
||||
///
|
||||
/// This detector emits the uniform candlestick sign convention shared across the
|
||||
/// pattern family — `−1.0` bearish, `0.0` no pattern — so it drops straight into
|
||||
/// a machine-learning feature matrix as a single dimension.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, GravestoneDoji, Indicator};
|
||||
///
|
||||
/// let mut indicator = GravestoneDoji::new();
|
||||
/// // Body at the bottom, long upper shadow.
|
||||
/// let candle = Candle::new(10.0, 14.0, 9.95, 10.0, 1.0, 0).unwrap();
|
||||
/// assert_eq!(indicator.update(candle), Some(-1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct GravestoneDoji {
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl GravestoneDoji {
|
||||
/// Construct a new Gravestone Doji detector.
|
||||
pub const fn new() -> Self {
|
||||
Self { has_emitted: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for GravestoneDoji {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let range = candle.high - candle.low;
|
||||
if range <= 0.0 {
|
||||
return Some(0.0);
|
||||
}
|
||||
if (candle.close - candle.open).abs() > 0.1 * range {
|
||||
return Some(0.0);
|
||||
}
|
||||
let upper = candle.high - candle.open.max(candle.close);
|
||||
let lower = candle.open.min(candle.close) - candle.low;
|
||||
if lower <= 0.1 * range && upper >= 0.5 * range {
|
||||
return Some(-1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"GravestoneDoji"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||
Candle::new(open, high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let t = GravestoneDoji::new();
|
||||
assert_eq!(t.name(), "GravestoneDoji");
|
||||
assert_eq!(t.warmup_period(), 1);
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gravestone_is_minus_one() {
|
||||
let mut t = GravestoneDoji::new();
|
||||
assert_eq!(t.update(c(10.0, 14.0, 9.95, 10.0, 0)), Some(-1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lower_shadow_yields_zero() {
|
||||
let mut t = GravestoneDoji::new();
|
||||
// Long lower shadow -> not a gravestone (this is a dragonfly shape).
|
||||
assert_eq!(t.update(c(10.0, 10.05, 6.0, 10.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_upper_shadow_yields_zero() {
|
||||
let mut t = GravestoneDoji::new();
|
||||
// Body at the bottom but the upper shadow is too short.
|
||||
assert_eq!(t.update(c(10.0, 10.4, 9.95, 10.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_doji_yields_zero() {
|
||||
let mut t = GravestoneDoji::new();
|
||||
assert_eq!(t.update(c(10.0, 14.0, 9.5, 13.5, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_range_yields_zero() {
|
||||
let mut t = GravestoneDoji::new();
|
||||
assert_eq!(t.update(c(10.0, 10.0, 10.0, 10.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 100.0 + i as f64;
|
||||
c(base, base + 4.0, base - 0.05, base, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = GravestoneDoji::new();
|
||||
let mut b = GravestoneDoji::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = GravestoneDoji::new();
|
||||
t.update(c(10.0, 14.0, 9.95, 10.0, 0));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,14 @@ use crate::traits::Indicator;
|
||||
/// check only — no trend filter is applied; combine with a trend indicator
|
||||
/// for actionable signals.
|
||||
///
|
||||
/// # Signed ±1 encoding
|
||||
///
|
||||
/// A Hammer is bullish by definition, so under the uniform candlestick sign
|
||||
/// convention (`+1.0` bullish, `−1.0` bearish, `0.0` none) it emits `+1.0`
|
||||
/// when the shape matches and `0.0` otherwise — it never emits `−1.0`. The
|
||||
/// same geometry read at the top of an uptrend is the bearish `HangingMan`,
|
||||
/// which carries the opposite sign.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
|
||||
@@ -22,6 +22,14 @@ use crate::traits::Indicator;
|
||||
/// check only — no trend filter is applied; combine with a trend indicator
|
||||
/// for actionable signals.
|
||||
///
|
||||
/// # Signed ±1 encoding
|
||||
///
|
||||
/// A Hanging Man is bearish by definition, so under the uniform candlestick
|
||||
/// sign convention (`+1.0` bullish, `−1.0` bearish, `0.0` none) it emits
|
||||
/// `−1.0` when the shape matches and `0.0` otherwise — it never emits `+1.0`.
|
||||
/// The same geometry read at the bottom of a downtrend is the bullish
|
||||
/// `Hammer`, which carries the opposite sign.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
|
||||
@@ -23,6 +23,13 @@ use crate::traits::Indicator;
|
||||
/// no trend filter is applied; combine with a trend indicator for actionable
|
||||
/// signals.
|
||||
///
|
||||
/// # Signed ±1 encoding
|
||||
///
|
||||
/// This detector already emits the uniform candlestick sign convention shared
|
||||
/// across the pattern family — `+1.0` bullish, `−1.0` bearish, `0.0` no
|
||||
/// pattern — so it drops straight into a machine-learning feature matrix where
|
||||
/// the bullish and bearish variants of the pattern occupy a single dimension.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
//! High-Wave candlestick pattern.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// High-Wave — a single-bar extreme-indecision signal. A small body with very
|
||||
/// long shadows on *both* sides: price swung far up and far down yet finished
|
||||
/// near the open, a sign that trend conviction has evaporated.
|
||||
///
|
||||
/// ```text
|
||||
/// range = high − low
|
||||
/// long upper = high − max(open, close) >= 0.4 * range
|
||||
/// long lower = min(open, close) − low >= 0.4 * range
|
||||
/// ```
|
||||
///
|
||||
/// The two long-shadow conditions force the body below `0.2 * range`, so no
|
||||
/// separate body test is needed. Output is `+1.0` when the high-wave prints and
|
||||
/// `0.0` otherwise — a non-directional indecision flag, it never emits `−1.0`.
|
||||
/// Shadow thresholds follow the geometric house style rather than TA-Lib's
|
||||
/// rolling averages. Pattern-shape check only — no trend filter is applied;
|
||||
/// combine with a trend indicator for actionable signals.
|
||||
///
|
||||
/// # Signed ±1 encoding
|
||||
///
|
||||
/// This detector emits the uniform candlestick sign convention shared across the
|
||||
/// pattern family — `+1.0` detected, `0.0` no pattern — so it drops straight into
|
||||
/// a machine-learning feature matrix as a single dimension.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, HighWave, Indicator};
|
||||
///
|
||||
/// let mut indicator = HighWave::new();
|
||||
/// // Small body, long shadows both sides.
|
||||
/// let candle = Candle::new(10.0, 12.0, 8.0, 10.3, 1.0, 0).unwrap();
|
||||
/// assert_eq!(indicator.update(candle), Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct HighWave {
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl HighWave {
|
||||
/// Construct a new High-Wave detector.
|
||||
pub const fn new() -> Self {
|
||||
Self { has_emitted: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for HighWave {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let range = candle.high - candle.low;
|
||||
if range <= 0.0 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let upper = candle.high - candle.open.max(candle.close);
|
||||
let lower = candle.open.min(candle.close) - candle.low;
|
||||
if upper >= 0.4 * range && lower >= 0.4 * range {
|
||||
return Some(1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"HighWave"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||
Candle::new(open, high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let t = HighWave::new();
|
||||
assert_eq!(t.name(), "HighWave");
|
||||
assert_eq!(t.warmup_period(), 1);
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_wave_is_plus_one() {
|
||||
let mut t = HighWave::new();
|
||||
assert_eq!(t.update(c(10.0, 12.0, 8.0, 10.3, 0)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_upper_shadow_yields_zero() {
|
||||
let mut t = HighWave::new();
|
||||
// Long lower shadow but short upper -> not a high-wave.
|
||||
assert_eq!(t.update(c(11.5, 12.0, 8.0, 11.7, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_lower_shadow_yields_zero() {
|
||||
let mut t = HighWave::new();
|
||||
// Long upper shadow but short lower -> not a high-wave.
|
||||
assert_eq!(t.update(c(8.3, 12.0, 8.0, 8.5, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn big_body_yields_zero() {
|
||||
let mut t = HighWave::new();
|
||||
// A large body cannot leave both shadows long.
|
||||
assert_eq!(t.update(c(8.5, 12.0, 8.0, 11.5, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_range_yields_zero() {
|
||||
let mut t = HighWave::new();
|
||||
assert_eq!(t.update(c(10.0, 10.0, 10.0, 10.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 100.0 + i as f64;
|
||||
c(base, base + 3.0, base - 3.0, base + 0.2, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = HighWave::new();
|
||||
let mut b = HighWave::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = HighWave::new();
|
||||
t.update(c(10.0, 12.0, 8.0, 10.3, 0));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
//! Hikkake candlestick pattern.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Hikkake — a 3-bar trap. An inside bar (bar2 fully contained by bar1) sets up a
|
||||
/// breakout that immediately fails on bar3, trapping breakout traders and pointing
|
||||
/// the opposite way.
|
||||
///
|
||||
/// ```text
|
||||
/// inside bar : bar2.high < bar1.high && bar2.low > bar1.low
|
||||
/// bullish (+1.0): bar3 makes a LOWER high AND LOWER low than bar2
|
||||
/// (a false downside break -> expect a move up)
|
||||
/// bearish (−1.0): bar3 makes a HIGHER high AND HIGHER low than bar2
|
||||
/// (a false upside break -> expect a move down)
|
||||
/// ```
|
||||
///
|
||||
/// Output is `+1.0` (bullish setup), `−1.0` (bearish setup), or `0.0` otherwise.
|
||||
/// The detector fires when the three-bar setup completes on bar3; it does not
|
||||
/// separately flag the optional later confirmation bar. The first two bars always
|
||||
/// return `0.0` because the window is not yet filled. Pattern-shape check only —
|
||||
/// no trend filter is applied; combine with a trend indicator for actionable
|
||||
/// signals.
|
||||
///
|
||||
/// # Signed ±1 encoding
|
||||
///
|
||||
/// This detector emits the uniform candlestick sign convention shared across the
|
||||
/// pattern family — `+1.0` bullish, `−1.0` bearish, `0.0` no pattern — so it
|
||||
/// drops straight into a machine-learning feature matrix where the bullish and
|
||||
/// bearish setups occupy a single dimension.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Hikkake, Indicator};
|
||||
///
|
||||
/// let mut indicator = Hikkake::new();
|
||||
/// indicator.update(Candle::new(10.0, 15.0, 5.0, 12.0, 1.0, 0).unwrap());
|
||||
/// indicator.update(Candle::new(11.0, 13.0, 8.0, 12.0, 1.0, 1).unwrap());
|
||||
/// let out = indicator
|
||||
/// .update(Candle::new(9.0, 12.0, 6.0, 7.0, 1.0, 2).unwrap());
|
||||
/// assert_eq!(out, Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Hikkake {
|
||||
prev: Option<Candle>,
|
||||
prev_prev: Option<Candle>,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl Hikkake {
|
||||
/// Construct a new Hikkake detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
prev: None,
|
||||
prev_prev: None,
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for Hikkake {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let bar1 = self.prev_prev;
|
||||
let bar2 = self.prev;
|
||||
self.prev_prev = self.prev;
|
||||
self.prev = Some(candle);
|
||||
let (Some(bar1), Some(bar2)) = (bar1, bar2) else {
|
||||
return Some(0.0);
|
||||
};
|
||||
// bar2 must be an inside bar of bar1.
|
||||
if !(bar2.high < bar1.high && bar2.low > bar1.low) {
|
||||
return Some(0.0);
|
||||
}
|
||||
// Bullish: bar3 breaks below the inside bar (lower high and lower low).
|
||||
if candle.high < bar2.high && candle.low < bar2.low {
|
||||
return Some(1.0);
|
||||
}
|
||||
// Bearish: bar3 breaks above the inside bar (higher high and higher low).
|
||||
if candle.high > bar2.high && candle.low > bar2.low {
|
||||
return Some(-1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.prev = None;
|
||||
self.prev_prev = None;
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
3
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"Hikkake"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||
Candle::new(open, high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let t = Hikkake::new();
|
||||
assert_eq!(t.name(), "Hikkake");
|
||||
assert_eq!(t.warmup_period(), 3);
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bullish_hikkake_is_plus_one() {
|
||||
let mut t = Hikkake::new();
|
||||
assert_eq!(t.update(c(10.0, 15.0, 5.0, 12.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(11.0, 13.0, 8.0, 12.0, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(9.0, 12.0, 6.0, 7.0, 2)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bearish_hikkake_is_minus_one() {
|
||||
let mut t = Hikkake::new();
|
||||
assert_eq!(t.update(c(10.0, 15.0, 5.0, 12.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(11.0, 13.0, 8.0, 12.0, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(12.0, 14.0, 9.0, 13.0, 2)), Some(-1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_inside_bar_yields_zero() {
|
||||
let mut t = Hikkake::new();
|
||||
t.update(c(10.0, 15.0, 5.0, 12.0, 0));
|
||||
// bar2 is not contained by bar1 (higher high).
|
||||
t.update(c(11.0, 16.0, 8.0, 12.0, 1));
|
||||
assert_eq!(t.update(c(9.0, 12.0, 6.0, 7.0, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outside_bar3_yields_zero() {
|
||||
let mut t = Hikkake::new();
|
||||
t.update(c(10.0, 15.0, 5.0, 12.0, 0));
|
||||
t.update(c(11.0, 13.0, 8.0, 12.0, 1));
|
||||
// bar3 engulfs bar2 (higher high and lower low) -> neither direction.
|
||||
assert_eq!(t.update(c(11.0, 14.0, 7.0, 9.0, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_two_bars_return_zero() {
|
||||
let mut t = Hikkake::new();
|
||||
assert_eq!(t.update(c(10.0, 15.0, 5.0, 12.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(11.0, 13.0, 8.0, 12.0, 1)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 100.0 + i as f64;
|
||||
match i % 3 {
|
||||
0 => c(base, base + 6.0, base - 6.0, base, i),
|
||||
1 => c(base, base + 2.0, base - 2.0, base, i),
|
||||
_ => c(base, base + 1.0, base - 5.0, base - 4.0, i),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let mut a = Hikkake::new();
|
||||
let mut b = Hikkake::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = Hikkake::new();
|
||||
t.update(c(10.0, 15.0, 5.0, 12.0, 0));
|
||||
t.update(c(11.0, 13.0, 8.0, 12.0, 1));
|
||||
t.update(c(9.0, 12.0, 6.0, 7.0, 2));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.update(c(10.0, 15.0, 5.0, 12.0, 0)), Some(0.0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
//! Modified Hikkake candlestick pattern.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Modified Hikkake — a close-confirmed variant of the [`Hikkake`](crate::Hikkake)
|
||||
/// trap. An inside bar is followed by a bar that breaks out *and is immediately
|
||||
/// rejected*: it pierces the inside bar's range intrabar but closes back inside,
|
||||
/// a stronger signal than the plain breakout setup.
|
||||
///
|
||||
/// ```text
|
||||
/// inside bar : bar2.high < bar1.high && bar2.low > bar1.low
|
||||
/// bullish (+1.0): bar3 makes a lower high AND lower low than bar2,
|
||||
/// yet closes back above the inside-bar low (close3 > bar2.low)
|
||||
/// bearish (−1.0): bar3 makes a higher high AND higher low than bar2,
|
||||
/// yet closes back below the inside-bar high (close3 < bar2.high)
|
||||
/// ```
|
||||
///
|
||||
/// Output is `+1.0` (bullish), `−1.0` (bearish), or `0.0` otherwise. The extra
|
||||
/// close-recovery condition is what distinguishes it from the plain Hikkake, which
|
||||
/// fires on the high/low break alone. The first two bars always return `0.0`
|
||||
/// because the three-bar window is not yet filled. Pattern-shape check only — no
|
||||
/// trend filter is applied; combine with a trend indicator for actionable signals.
|
||||
///
|
||||
/// # Signed ±1 encoding
|
||||
///
|
||||
/// This detector emits the uniform candlestick sign convention shared across the
|
||||
/// pattern family — `+1.0` bullish, `−1.0` bearish, `0.0` no pattern — so it
|
||||
/// drops straight into a machine-learning feature matrix as a single dimension.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, HikkakeModified, Indicator};
|
||||
///
|
||||
/// let mut indicator = HikkakeModified::new();
|
||||
/// indicator.update(Candle::new(10.0, 15.0, 5.0, 12.0, 1.0, 0).unwrap());
|
||||
/// indicator.update(Candle::new(11.0, 13.0, 8.0, 12.0, 1.0, 1).unwrap());
|
||||
/// let out = indicator
|
||||
/// .update(Candle::new(9.0, 12.0, 6.0, 9.0, 1.0, 2).unwrap());
|
||||
/// assert_eq!(out, Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct HikkakeModified {
|
||||
prev: Option<Candle>,
|
||||
prev_prev: Option<Candle>,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl HikkakeModified {
|
||||
/// Construct a new Modified Hikkake detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
prev: None,
|
||||
prev_prev: None,
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for HikkakeModified {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let bar1 = self.prev_prev;
|
||||
let bar2 = self.prev;
|
||||
self.prev_prev = self.prev;
|
||||
self.prev = Some(candle);
|
||||
let (Some(bar1), Some(bar2)) = (bar1, bar2) else {
|
||||
return Some(0.0);
|
||||
};
|
||||
if !(bar2.high < bar1.high && bar2.low > bar1.low) {
|
||||
return Some(0.0);
|
||||
}
|
||||
// Bullish: false downside break that closes back above the inside-bar low.
|
||||
if candle.high < bar2.high && candle.low < bar2.low && candle.close > bar2.low {
|
||||
return Some(1.0);
|
||||
}
|
||||
// Bearish: false upside break that closes back below the inside-bar high.
|
||||
if candle.high > bar2.high && candle.low > bar2.low && candle.close < bar2.high {
|
||||
return Some(-1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.prev = None;
|
||||
self.prev_prev = None;
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
3
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"HikkakeModified"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||
Candle::new(open, high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let t = HikkakeModified::new();
|
||||
assert_eq!(t.name(), "HikkakeModified");
|
||||
assert_eq!(t.warmup_period(), 3);
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bullish_modified_hikkake_is_plus_one() {
|
||||
let mut t = HikkakeModified::new();
|
||||
assert_eq!(t.update(c(10.0, 15.0, 5.0, 12.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(11.0, 13.0, 8.0, 12.0, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(9.0, 12.0, 6.0, 9.0, 2)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bearish_modified_hikkake_is_minus_one() {
|
||||
let mut t = HikkakeModified::new();
|
||||
assert_eq!(t.update(c(10.0, 15.0, 5.0, 12.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(11.0, 13.0, 8.0, 12.0, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(13.0, 14.0, 9.0, 10.0, 2)), Some(-1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn break_without_close_recovery_yields_zero() {
|
||||
let mut t = HikkakeModified::new();
|
||||
t.update(c(10.0, 15.0, 5.0, 12.0, 0));
|
||||
t.update(c(11.0, 13.0, 8.0, 12.0, 1));
|
||||
// Lower high and lower low, but closes below the inside-bar low -> plain
|
||||
// Hikkake break, not the close-confirmed modified version.
|
||||
assert_eq!(t.update(c(9.0, 12.0, 6.0, 7.0, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_inside_bar_yields_zero() {
|
||||
let mut t = HikkakeModified::new();
|
||||
t.update(c(10.0, 15.0, 5.0, 12.0, 0));
|
||||
t.update(c(11.0, 16.0, 8.0, 12.0, 1));
|
||||
assert_eq!(t.update(c(9.0, 12.0, 6.0, 9.0, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_two_bars_return_zero() {
|
||||
let mut t = HikkakeModified::new();
|
||||
assert_eq!(t.update(c(10.0, 15.0, 5.0, 12.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(11.0, 13.0, 8.0, 12.0, 1)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 100.0 + i as f64;
|
||||
match i % 3 {
|
||||
0 => c(base, base + 6.0, base - 6.0, base, i),
|
||||
1 => c(base, base + 2.0, base - 2.0, base, i),
|
||||
_ => c(base, base + 1.0, base - 5.0, base, i),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let mut a = HikkakeModified::new();
|
||||
let mut b = HikkakeModified::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = HikkakeModified::new();
|
||||
t.update(c(10.0, 15.0, 5.0, 12.0, 0));
|
||||
t.update(c(11.0, 13.0, 8.0, 12.0, 1));
|
||||
t.update(c(9.0, 12.0, 6.0, 9.0, 2));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.update(c(10.0, 15.0, 5.0, 12.0, 0)), Some(0.0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
//! Homing Pigeon candlestick pattern.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Homing Pigeon — a 2-bar bullish reversal. Two black candles in a decline, the
|
||||
/// second a small body sitting entirely inside the first body (a same-colour
|
||||
/// harami). The shrinking range signals selling pressure is fading.
|
||||
///
|
||||
/// ```text
|
||||
/// bar1 black (close < open)
|
||||
/// bar2 black & its body sits inside bar1's body
|
||||
/// (open2 <= open1 && close2 >= close1)
|
||||
/// bar2 body is smaller than bar1's
|
||||
/// ```
|
||||
///
|
||||
/// Output is `+1.0` when the pattern completes and `0.0` otherwise. Homing Pigeon
|
||||
/// is a single-direction (bullish-only) reversal, so it never emits `−1.0`. The
|
||||
/// first bar always returns `0.0` because the two-bar window is not yet filled.
|
||||
/// Pattern-shape check only — no trend filter is applied; combine with a trend
|
||||
/// indicator for actionable signals.
|
||||
///
|
||||
/// # Signed ±1 encoding
|
||||
///
|
||||
/// This detector emits the uniform candlestick sign convention shared across the
|
||||
/// pattern family — `+1.0` bullish, `0.0` no pattern — so it drops straight into
|
||||
/// a machine-learning feature matrix as a single dimension.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, HomingPigeon, Indicator};
|
||||
///
|
||||
/// let mut indicator = HomingPigeon::new();
|
||||
/// indicator.update(Candle::new(15.0, 15.1, 9.9, 10.0, 1.0, 0).unwrap());
|
||||
/// let out = indicator
|
||||
/// .update(Candle::new(14.0, 14.1, 10.9, 11.0, 1.0, 1).unwrap());
|
||||
/// assert_eq!(out, Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct HomingPigeon {
|
||||
prev: Option<Candle>,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl HomingPigeon {
|
||||
/// Construct a new Homing Pigeon detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
prev: None,
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for HomingPigeon {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let prev = self.prev;
|
||||
self.prev = Some(candle);
|
||||
let Some(bar1) = prev else {
|
||||
return Some(0.0);
|
||||
};
|
||||
// Both bars black, bar2's body inside bar1's body and smaller.
|
||||
if bar1.close < bar1.open
|
||||
&& candle.close < candle.open
|
||||
&& candle.open <= bar1.open
|
||||
&& candle.close >= bar1.close
|
||||
&& (candle.open - candle.close) < (bar1.open - bar1.close)
|
||||
{
|
||||
return Some(1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.prev = None;
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
2
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"HomingPigeon"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||
Candle::new(open, high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let t = HomingPigeon::new();
|
||||
assert_eq!(t.name(), "HomingPigeon");
|
||||
assert_eq!(t.warmup_period(), 2);
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn homing_pigeon_is_plus_one() {
|
||||
let mut t = HomingPigeon::new();
|
||||
assert_eq!(t.update(c(15.0, 15.1, 9.9, 10.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(14.0, 14.1, 10.9, 11.0, 1)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn second_bar_white_yields_zero() {
|
||||
let mut t = HomingPigeon::new();
|
||||
t.update(c(15.0, 15.1, 9.9, 10.0, 0));
|
||||
// bar2 white -> not a homing pigeon.
|
||||
assert_eq!(t.update(c(11.0, 14.1, 10.9, 14.0, 1)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn second_body_not_inside_yields_zero() {
|
||||
let mut t = HomingPigeon::new();
|
||||
t.update(c(15.0, 15.1, 9.9, 10.0, 0));
|
||||
// bar2 opens above bar1's open -> body not contained.
|
||||
assert_eq!(t.update(c(16.0, 16.1, 10.9, 11.0, 1)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_bar_returns_zero() {
|
||||
let mut t = HomingPigeon::new();
|
||||
assert_eq!(t.update(c(15.0, 15.1, 9.9, 10.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 100.0 + i as f64;
|
||||
c(base + 5.0, base + 5.1, base - 0.1, base, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = HomingPigeon::new();
|
||||
let mut b = HomingPigeon::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = HomingPigeon::new();
|
||||
t.update(c(15.0, 15.1, 9.9, 10.0, 0));
|
||||
t.update(c(14.0, 14.1, 10.9, 11.0, 1));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.update(c(15.0, 15.1, 9.9, 10.0, 0)), Some(0.0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
//! Identical Three Crows candlestick pattern.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Identical Three Crows — a 3-bar bearish reversal: three consecutive red
|
||||
/// candles with steadily lower closes where each candle opens at (or very near)
|
||||
/// the prior candle's close, so the bodies stack in an identical staircase.
|
||||
///
|
||||
/// ```text
|
||||
/// tol_n = tolerance * max(|open|, |prev.close|)
|
||||
/// all three red (close < open)
|
||||
/// declining closes (bar2.close < bar1.close, bar3.close < bar2.close)
|
||||
/// bar2 opens at bar1's close (|bar2.open − bar1.close| <= tol_2)
|
||||
/// bar3 opens at bar2's close (|bar3.open − bar2.close| <= tol_3)
|
||||
/// ```
|
||||
///
|
||||
/// Output is `−1.0` when the pattern completes and `0.0` otherwise. Identical
|
||||
/// Three Crows is a single-direction (bearish-only) pattern, so it never emits
|
||||
/// `+1.0`. The first two bars always return `0.0` because the three-bar window
|
||||
/// is not yet filled. `tolerance` defaults to `0.001` (10 bps relative) and must
|
||||
/// lie in `[0, 1)`. Pattern-shape check only — no trend filter is applied;
|
||||
/// combine with a trend indicator for actionable signals.
|
||||
///
|
||||
/// # Signed ±1 encoding
|
||||
///
|
||||
/// This detector emits the uniform candlestick sign convention shared across the
|
||||
/// pattern family — `−1.0` bearish, `0.0` no pattern — so it drops straight into
|
||||
/// a machine-learning feature matrix as a single dimension.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, IdenticalThreeCrows, Indicator};
|
||||
///
|
||||
/// let mut indicator = IdenticalThreeCrows::new();
|
||||
/// indicator.update(Candle::new(13.0, 13.1, 11.9, 12.0, 1.0, 0).unwrap());
|
||||
/// indicator.update(Candle::new(12.0, 12.1, 10.9, 11.0, 1.0, 1).unwrap());
|
||||
/// let out = indicator
|
||||
/// .update(Candle::new(11.0, 11.1, 9.9, 10.0, 1.0, 2).unwrap());
|
||||
/// assert_eq!(out, Some(-1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct IdenticalThreeCrows {
|
||||
tolerance: f64,
|
||||
prev: Option<Candle>,
|
||||
prev_prev: Option<Candle>,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl Default for IdenticalThreeCrows {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl IdenticalThreeCrows {
|
||||
/// Construct a detector with the default relative tolerance (1e-3).
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
tolerance: 0.001,
|
||||
prev: None,
|
||||
prev_prev: None,
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a detector with a custom relative tolerance.
|
||||
///
|
||||
/// `tolerance` must lie in `[0, 1)`.
|
||||
pub fn with_tolerance(tolerance: f64) -> Result<Self> {
|
||||
if !(0.0..1.0).contains(&tolerance) {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "identical three crows tolerance must lie in [0, 1)",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
tolerance,
|
||||
prev: None,
|
||||
prev_prev: None,
|
||||
has_emitted: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured relative tolerance.
|
||||
pub fn tolerance(&self) -> f64 {
|
||||
self.tolerance
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for IdenticalThreeCrows {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let pp = self.prev_prev;
|
||||
let p = self.prev;
|
||||
self.prev_prev = self.prev;
|
||||
self.prev = Some(candle);
|
||||
let (Some(bar1), Some(bar2)) = (pp, p) else {
|
||||
return Some(0.0);
|
||||
};
|
||||
let tol2 = self.tolerance * bar2.open.abs().max(bar1.close.abs());
|
||||
let tol3 = self.tolerance * candle.open.abs().max(bar2.close.abs());
|
||||
if bar1.close < bar1.open
|
||||
&& bar2.close < bar2.open
|
||||
&& candle.close < candle.open
|
||||
&& bar2.close < bar1.close
|
||||
&& candle.close < bar2.close
|
||||
&& (bar2.open - bar1.close).abs() <= tol2
|
||||
&& (candle.open - bar2.close).abs() <= tol3
|
||||
{
|
||||
return Some(-1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.prev = None;
|
||||
self.prev_prev = None;
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
3
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"IdenticalThreeCrows"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||
Candle::new(open, high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_tolerance() {
|
||||
assert!(IdenticalThreeCrows::with_tolerance(-0.01).is_err());
|
||||
assert!(IdenticalThreeCrows::with_tolerance(1.0).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_valid_tolerance() {
|
||||
let t = IdenticalThreeCrows::with_tolerance(0.0).unwrap();
|
||||
assert!((t.tolerance() - 0.0).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let t = IdenticalThreeCrows::default();
|
||||
assert_eq!(t.name(), "IdenticalThreeCrows");
|
||||
assert_eq!(t.warmup_period(), 3);
|
||||
assert!(!t.is_ready());
|
||||
assert!((t.tolerance() - 0.001).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identical_three_crows_is_minus_one() {
|
||||
let mut t = IdenticalThreeCrows::new();
|
||||
// Three red candles, each opening at the prior close, declining.
|
||||
assert_eq!(t.update(c(13.0, 13.1, 11.9, 12.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(12.0, 12.1, 10.9, 11.0, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(11.0, 11.1, 9.9, 10.0, 2)), Some(-1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_identical_opens_yield_zero() {
|
||||
let mut t = IdenticalThreeCrows::new();
|
||||
t.update(c(13.0, 13.1, 11.9, 12.0, 0));
|
||||
t.update(c(12.0, 12.1, 10.9, 11.0, 1));
|
||||
// bar3 opens at 10.0, far from bar2's close (11.0) -> not identical.
|
||||
assert_eq!(t.update(c(10.0, 10.1, 8.9, 9.0, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rising_close_yields_zero() {
|
||||
let mut t = IdenticalThreeCrows::new();
|
||||
t.update(c(13.0, 13.1, 11.9, 12.0, 0));
|
||||
t.update(c(12.0, 12.1, 10.9, 11.0, 1));
|
||||
// bar3 is green -> not three crows.
|
||||
assert_eq!(t.update(c(11.0, 12.2, 10.9, 12.0, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_two_bars_return_zero() {
|
||||
let mut t = IdenticalThreeCrows::new();
|
||||
assert_eq!(t.update(c(13.0, 13.1, 11.9, 12.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(12.0, 12.1, 10.9, 11.0, 1)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 100.0 - i as f64;
|
||||
c(base, base + 0.1, base - 1.1, base - 1.0, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = IdenticalThreeCrows::new();
|
||||
let mut b = IdenticalThreeCrows::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = IdenticalThreeCrows::new();
|
||||
t.update(c(13.0, 13.1, 11.9, 12.0, 0));
|
||||
t.update(c(12.0, 12.1, 10.9, 11.0, 1));
|
||||
t.update(c(11.0, 11.1, 9.9, 10.0, 2));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.update(c(13.0, 13.1, 11.9, 12.0, 0)), Some(0.0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
//! In-Neck candlestick pattern.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// In-Neck — a 2-bar bearish continuation, slightly stronger than On-Neck. A long
|
||||
/// black candle in a decline is followed by a white candle that opens below the
|
||||
/// black bar's low and closes just barely *into* the black body, around its close
|
||||
/// level. The shallow recovery still favours the sellers.
|
||||
///
|
||||
/// ```text
|
||||
/// long body = |close − open| >= 0.5 * (high − low)
|
||||
/// bar1 black & long
|
||||
/// bar2 white, opens below bar1's low (open2 < low1)
|
||||
/// bar2 closes just into bar1's body (close1 <= close2 <= close1 + 0.1 · body1)
|
||||
/// ```
|
||||
///
|
||||
/// Output is `−1.0` when the pattern completes and `0.0` otherwise. In-Neck is a
|
||||
/// single-direction (bearish-only) continuation, so it never emits `+1.0`. The
|
||||
/// first bar always returns `0.0` because the two-bar window is not yet filled.
|
||||
/// Body and neckline thresholds follow the geometric house style rather than
|
||||
/// TA-Lib's rolling averages. Pattern-shape check only — no trend filter is
|
||||
/// applied; combine with a trend indicator for actionable signals.
|
||||
///
|
||||
/// # Signed ±1 encoding
|
||||
///
|
||||
/// This detector emits the uniform candlestick sign convention shared across the
|
||||
/// pattern family — `−1.0` bearish, `0.0` no pattern — so it drops straight into
|
||||
/// a machine-learning feature matrix as a single dimension.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, InNeck, Indicator};
|
||||
///
|
||||
/// let mut indicator = InNeck::new();
|
||||
/// indicator.update(Candle::new(15.0, 15.1, 9.0, 10.0, 1.0, 0).unwrap());
|
||||
/// let out = indicator
|
||||
/// .update(Candle::new(7.0, 10.3, 6.9, 10.2, 1.0, 1).unwrap());
|
||||
/// assert_eq!(out, Some(-1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct InNeck {
|
||||
prev: Option<Candle>,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl InNeck {
|
||||
/// Construct a new In-Neck detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
prev: None,
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for InNeck {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let prev = self.prev;
|
||||
self.prev = Some(candle);
|
||||
let Some(bar1) = prev else {
|
||||
return Some(0.0);
|
||||
};
|
||||
let range1 = bar1.high - bar1.low;
|
||||
if range1 <= 0.0 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let body1 = bar1.open - bar1.close;
|
||||
if bar1.close < bar1.open
|
||||
&& body1 >= 0.5 * range1
|
||||
&& candle.close > candle.open
|
||||
&& candle.open < bar1.low
|
||||
&& candle.close >= bar1.close
|
||||
&& candle.close <= bar1.close + 0.1 * body1
|
||||
{
|
||||
return Some(-1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.prev = None;
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
2
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"InNeck"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||
Candle::new(open, high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let t = InNeck::new();
|
||||
assert_eq!(t.name(), "InNeck");
|
||||
assert_eq!(t.warmup_period(), 2);
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn in_neck_is_minus_one() {
|
||||
let mut t = InNeck::new();
|
||||
assert_eq!(t.update(c(15.0, 15.1, 9.0, 10.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(7.0, 10.3, 6.9, 10.2, 1)), Some(-1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn close_at_low_yields_zero() {
|
||||
let mut t = InNeck::new();
|
||||
t.update(c(15.0, 15.1, 9.0, 10.0, 0));
|
||||
// Closes at the prior low, not into the body -> on-neck, not in-neck.
|
||||
assert_eq!(t.update(c(7.0, 9.1, 6.9, 9.0, 1)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn close_past_neck_yields_zero() {
|
||||
let mut t = InNeck::new();
|
||||
t.update(c(15.0, 15.1, 9.0, 10.0, 0));
|
||||
// Closes well into the body -> thrusting, not in-neck.
|
||||
assert_eq!(t.update(c(7.0, 11.6, 6.9, 11.5, 1)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn second_bar_black_yields_zero() {
|
||||
let mut t = InNeck::new();
|
||||
t.update(c(15.0, 15.1, 9.0, 10.0, 0));
|
||||
assert_eq!(t.update(c(10.4, 10.5, 6.9, 10.1, 1)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_bar_returns_zero() {
|
||||
let mut t = InNeck::new();
|
||||
assert_eq!(t.update(c(15.0, 15.1, 9.0, 10.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 100.0 + i as f64;
|
||||
c(base + 5.0, base + 5.1, base - 1.0, base, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = InNeck::new();
|
||||
let mut b = InNeck::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = InNeck::new();
|
||||
t.update(c(15.0, 15.1, 9.0, 10.0, 0));
|
||||
t.update(c(7.0, 10.3, 6.9, 10.2, 1));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.update(c(15.0, 15.1, 9.0, 10.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_range_first_bar_yields_zero() {
|
||||
let mut t = InNeck::new();
|
||||
// Flat first bar (range1 == 0) -> rejected.
|
||||
t.update(c(10.0, 10.0, 10.0, 10.0, 0));
|
||||
assert_eq!(t.update(c(9.0, 10.0, 8.0, 9.5, 1)), Some(0.0));
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,14 @@ use crate::traits::Indicator;
|
||||
/// check only — no trend filter is applied; combine with a trend indicator
|
||||
/// for actionable signals.
|
||||
///
|
||||
/// # Signed ±1 encoding
|
||||
///
|
||||
/// An Inverted Hammer is bullish by definition, so under the uniform
|
||||
/// candlestick sign convention (`+1.0` bullish, `−1.0` bearish, `0.0` none) it
|
||||
/// emits `+1.0` when the shape matches and `0.0` otherwise — it never emits
|
||||
/// `−1.0`. The same geometry read at the top of an uptrend is the bearish
|
||||
/// `ShootingStar`, which carries the opposite sign.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
//! Kicking candlestick pattern.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Kicking — a 2-bar reversal of two opposite-coloured marubozu separated by a
|
||||
/// gap. A shadowless candle is "kicked" the other way by a shadowless candle of
|
||||
/// the opposite colour that gaps clear of it — a violent change of control. It is
|
||||
/// trend-agnostic: the gap direction alone defines the signal.
|
||||
///
|
||||
/// ```text
|
||||
/// marubozu = |close − open| >= 0.95 * (high − low) (no meaningful shadows)
|
||||
/// bullish (+1.0): black marubozu, then a white marubozu gapping UP (low2 > high1)
|
||||
/// bearish (−1.0): white marubozu, then a black marubozu gapping DOWN (high2 < low1)
|
||||
/// ```
|
||||
///
|
||||
/// Output is `+1.0` (bullish) or `−1.0` (bearish) when the pattern completes and
|
||||
/// `0.0` otherwise. The first bar always returns `0.0` because the two-bar window
|
||||
/// is not yet filled. The marubozu threshold follows the geometric house style
|
||||
/// rather than TA-Lib's rolling averages. Pattern-shape check only — no trend
|
||||
/// filter is applied; combine with a trend indicator for actionable signals.
|
||||
///
|
||||
/// # Signed ±1 encoding
|
||||
///
|
||||
/// This detector emits the uniform candlestick sign convention shared across the
|
||||
/// pattern family — `+1.0` bullish, `−1.0` bearish, `0.0` no pattern — so it
|
||||
/// drops straight into a machine-learning feature matrix where the two directions
|
||||
/// occupy a single dimension.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, Kicking};
|
||||
///
|
||||
/// let mut indicator = Kicking::new();
|
||||
/// indicator.update(Candle::new(12.0, 12.0, 10.0, 10.0, 1.0, 0).unwrap());
|
||||
/// let out = indicator
|
||||
/// .update(Candle::new(14.0, 16.0, 14.0, 16.0, 1.0, 1).unwrap());
|
||||
/// assert_eq!(out, Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Kicking {
|
||||
prev: Option<Candle>,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl Kicking {
|
||||
/// Construct a new Kicking detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
prev: None,
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `candle` is a marubozu (body fills at least 95 % of its range).
|
||||
fn is_marubozu(candle: &Candle) -> bool {
|
||||
let range = candle.high - candle.low;
|
||||
range > 0.0 && (candle.close - candle.open).abs() >= 0.95 * range
|
||||
}
|
||||
|
||||
impl Indicator for Kicking {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let prev = self.prev;
|
||||
self.prev = Some(candle);
|
||||
let Some(bar1) = prev else {
|
||||
return Some(0.0);
|
||||
};
|
||||
if !is_marubozu(&bar1) || !is_marubozu(&candle) {
|
||||
return Some(0.0);
|
||||
}
|
||||
// Bullish: black marubozu kicked up by a white marubozu gapping above it.
|
||||
if bar1.close < bar1.open && candle.close > candle.open && candle.low > bar1.high {
|
||||
return Some(1.0);
|
||||
}
|
||||
// Bearish: white marubozu kicked down by a black marubozu gapping below it.
|
||||
if bar1.close > bar1.open && candle.close < candle.open && candle.high < bar1.low {
|
||||
return Some(-1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.prev = None;
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
2
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"Kicking"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||
Candle::new(open, high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let t = Kicking::new();
|
||||
assert_eq!(t.name(), "Kicking");
|
||||
assert_eq!(t.warmup_period(), 2);
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bullish_kicking_is_plus_one() {
|
||||
let mut t = Kicking::new();
|
||||
assert_eq!(t.update(c(12.0, 12.0, 10.0, 10.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(14.0, 16.0, 14.0, 16.0, 1)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bearish_kicking_is_minus_one() {
|
||||
let mut t = Kicking::new();
|
||||
assert_eq!(t.update(c(10.0, 12.0, 10.0, 12.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(8.0, 8.0, 6.0, 6.0, 1)), Some(-1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_marubozu_yields_zero() {
|
||||
let mut t = Kicking::new();
|
||||
// bar1 has long shadows -> not a marubozu.
|
||||
t.update(c(12.0, 14.0, 8.0, 10.0, 0));
|
||||
assert_eq!(t.update(c(14.0, 16.0, 14.0, 16.0, 1)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_gap_yields_zero() {
|
||||
let mut t = Kicking::new();
|
||||
t.update(c(12.0, 12.0, 10.0, 10.0, 0));
|
||||
// White marubozu but it overlaps bar1 (no gap up).
|
||||
assert_eq!(t.update(c(11.0, 13.0, 11.0, 13.0, 1)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_bar_returns_zero() {
|
||||
let mut t = Kicking::new();
|
||||
assert_eq!(t.update(c(12.0, 12.0, 10.0, 10.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 100.0 + i as f64 * 5.0;
|
||||
if i % 2 == 0 {
|
||||
c(base + 2.0, base + 2.0, base, base, i)
|
||||
} else {
|
||||
c(base + 3.0, base + 5.0, base + 3.0, base + 5.0, i)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let mut a = Kicking::new();
|
||||
let mut b = Kicking::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = Kicking::new();
|
||||
t.update(c(12.0, 12.0, 10.0, 10.0, 0));
|
||||
t.update(c(14.0, 16.0, 14.0, 16.0, 1));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.update(c(12.0, 12.0, 10.0, 10.0, 0)), Some(0.0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
//! Kicking-by-Length candlestick pattern.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Kicking-by-Length — the [`Kicking`](crate::Kicking) pattern with the signal
|
||||
/// taken from the *longer* of the two marubozu rather than from the gap direction.
|
||||
/// When the two shadowless candles differ in size, the bigger one is treated as
|
||||
/// the dominant force.
|
||||
///
|
||||
/// ```text
|
||||
/// marubozu = |close − open| >= 0.95 * (high − low)
|
||||
/// setup: two opposite-coloured marubozu separated by a gap
|
||||
/// black then white gapping UP, or white then black gapping DOWN
|
||||
/// signal = colour of the LONGER marubozu (white -> +1.0, black -> −1.0)
|
||||
/// ```
|
||||
///
|
||||
/// Output is `+1.0` or `−1.0` when the kicking setup is present and `0.0`
|
||||
/// otherwise. Note this can disagree with [`Kicking`](crate::Kicking): a black
|
||||
/// marubozu kicked up by a *shorter* white marubozu reports `−1.0` here. The first
|
||||
/// bar always returns `0.0` because the two-bar window is not yet filled. The
|
||||
/// marubozu threshold follows the geometric house style rather than TA-Lib's
|
||||
/// rolling averages. Pattern-shape check only — no trend filter is applied;
|
||||
/// combine with a trend indicator for actionable signals.
|
||||
///
|
||||
/// # Signed ±1 encoding
|
||||
///
|
||||
/// This detector emits the uniform candlestick sign convention shared across the
|
||||
/// pattern family — `+1.0` bullish, `−1.0` bearish, `0.0` no pattern — so it
|
||||
/// drops straight into a machine-learning feature matrix as a single dimension.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, KickingByLength};
|
||||
///
|
||||
/// let mut indicator = KickingByLength::new();
|
||||
/// indicator.update(Candle::new(12.0, 12.0, 10.0, 10.0, 1.0, 0).unwrap());
|
||||
/// // White marubozu gaps up and is the longer body -> +1.
|
||||
/// let out = indicator
|
||||
/// .update(Candle::new(14.0, 20.0, 14.0, 20.0, 1.0, 1).unwrap());
|
||||
/// assert_eq!(out, Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct KickingByLength {
|
||||
prev: Option<Candle>,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl KickingByLength {
|
||||
/// Construct a new Kicking-by-Length detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
prev: None,
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_marubozu(candle: &Candle) -> bool {
|
||||
let range = candle.high - candle.low;
|
||||
range > 0.0 && (candle.close - candle.open).abs() >= 0.95 * range
|
||||
}
|
||||
|
||||
impl Indicator for KickingByLength {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let prev = self.prev;
|
||||
self.prev = Some(candle);
|
||||
let Some(bar1) = prev else {
|
||||
return Some(0.0);
|
||||
};
|
||||
if !is_marubozu(&bar1) || !is_marubozu(&candle) {
|
||||
return Some(0.0);
|
||||
}
|
||||
let body1 = bar1.close - bar1.open;
|
||||
let body2 = candle.close - candle.open;
|
||||
let bullish_setup = body1 < 0.0 && body2 > 0.0 && candle.low > bar1.high;
|
||||
let bearish_setup = body1 > 0.0 && body2 < 0.0 && candle.high < bar1.low;
|
||||
if !(bullish_setup || bearish_setup) {
|
||||
return Some(0.0);
|
||||
}
|
||||
// The longer marubozu's colour is the signal.
|
||||
let longer_is_white = if body1.abs() >= body2.abs() {
|
||||
body1 > 0.0
|
||||
} else {
|
||||
body2 > 0.0
|
||||
};
|
||||
Some(if longer_is_white { 1.0 } else { -1.0 })
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.prev = None;
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
2
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"KickingByLength"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||
Candle::new(open, high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let t = KickingByLength::new();
|
||||
assert_eq!(t.name(), "KickingByLength");
|
||||
assert_eq!(t.warmup_period(), 2);
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn longer_white_is_plus_one() {
|
||||
let mut t = KickingByLength::new();
|
||||
assert_eq!(t.update(c(12.0, 12.0, 10.0, 10.0, 0)), Some(0.0));
|
||||
// White marubozu (length 6) longer than the black one (length 2).
|
||||
assert_eq!(t.update(c(14.0, 20.0, 14.0, 20.0, 1)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn longer_black_is_minus_one() {
|
||||
let mut t = KickingByLength::new();
|
||||
// Black marubozu (length 6), then a shorter white marubozu (length 2)
|
||||
// gapping up -> the longer black body wins, so -1.
|
||||
assert_eq!(t.update(c(16.0, 16.0, 10.0, 10.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(18.0, 20.0, 18.0, 20.0, 1)), Some(-1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_marubozu_yields_zero() {
|
||||
let mut t = KickingByLength::new();
|
||||
t.update(c(12.0, 14.0, 8.0, 10.0, 0));
|
||||
assert_eq!(t.update(c(14.0, 20.0, 14.0, 20.0, 1)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_gap_yields_zero() {
|
||||
let mut t = KickingByLength::new();
|
||||
t.update(c(12.0, 12.0, 10.0, 10.0, 0));
|
||||
assert_eq!(t.update(c(11.0, 13.0, 11.0, 13.0, 1)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_bar_returns_zero() {
|
||||
let mut t = KickingByLength::new();
|
||||
assert_eq!(t.update(c(12.0, 12.0, 10.0, 10.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 100.0 + i as f64 * 5.0;
|
||||
if i % 2 == 0 {
|
||||
c(base + 2.0, base + 2.0, base, base, i)
|
||||
} else {
|
||||
c(base + 3.0, base + 5.0, base + 3.0, base + 5.0, i)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let mut a = KickingByLength::new();
|
||||
let mut b = KickingByLength::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = KickingByLength::new();
|
||||
t.update(c(12.0, 12.0, 10.0, 10.0, 0));
|
||||
t.update(c(14.0, 20.0, 14.0, 20.0, 1));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.update(c(12.0, 12.0, 10.0, 10.0, 0)), Some(0.0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
//! Kyle's Lambda — rolling price impact per unit of signed order flow.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::microstructure::TradeQuote;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Kyle's Lambda — the rolling ordinary-least-squares slope of mid-price changes
|
||||
/// on signed trade volume, the canonical measure of market depth / price
|
||||
/// impact.
|
||||
///
|
||||
/// Each `update` receives a [`TradeQuote`] — a trade plus the mid prevailing at
|
||||
/// execution. Internally the indicator forms, per trade, the mid change since
|
||||
/// the previous trade (`Δmid = midₜ − midₜ₋₁`) and the signed volume
|
||||
/// (`q = size · D`, with `D` the aggressor sign), then runs a rolling OLS
|
||||
/// regression of `Δmid` on `q` over the trailing window of `window` trades:
|
||||
///
|
||||
/// ```text
|
||||
/// cov = (1/n) · Σ q·Δmid − q̄·Δ̄mid
|
||||
/// var = (1/n) · Σ q² − q̄²
|
||||
/// λ = cov / var
|
||||
/// ```
|
||||
///
|
||||
/// `λ` is the estimated price move per unit of signed volume: a deep, liquid
|
||||
/// book absorbs flow with little movement and reads a small `λ`; a thin book
|
||||
/// moves sharply per unit traded and reads a large `λ`. It is a direct,
|
||||
/// model-light proxy for the slope of the demand curve in Kyle's microstructure
|
||||
/// model.
|
||||
///
|
||||
/// Each `update` is O(1): four running sums (`Σq`, `ΣΔmid`, `Σq²`, `Σq·Δmid`)
|
||||
/// are maintained as the window slides. A window of constant signed volume has
|
||||
/// zero variance and `λ` is undefined; the indicator returns `0` in that case
|
||||
/// rather than producing `NaN`.
|
||||
///
|
||||
/// `Input = TradeQuote`, `Output = f64`. It warms up for `window + 1`
|
||||
/// trade-quotes: one to seed the previous mid, then `window` paired
|
||||
/// observations.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Indicator, KylesLambda, Side, Trade, TradeQuote};
|
||||
///
|
||||
/// // A book where each trade moves the mid by exactly 0.5 per unit of signed
|
||||
/// // volume gives λ = 0.5.
|
||||
/// let mut lambda = KylesLambda::new(8).unwrap();
|
||||
/// let mut mid = 100.0;
|
||||
/// let mut last = None;
|
||||
/// for i in 0..20 {
|
||||
/// let side = if i % 2 == 0 { Side::Buy } else { Side::Sell };
|
||||
/// let size = 1.0 + f64::from(i % 3);
|
||||
/// let signed = size * side.sign();
|
||||
/// mid += 0.5 * signed;
|
||||
/// let trade = Trade::new(mid, size, side, 0).unwrap();
|
||||
/// last = lambda.update(TradeQuote::new(trade, mid).unwrap());
|
||||
/// }
|
||||
/// assert!((last.unwrap() - 0.5).abs() < 1e-9);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct KylesLambda {
|
||||
window: usize,
|
||||
prev_mid: Option<f64>,
|
||||
pairs: VecDeque<(f64, f64)>,
|
||||
sum_q: f64,
|
||||
sum_dm: f64,
|
||||
sum_qq: f64,
|
||||
sum_qdm: f64,
|
||||
}
|
||||
|
||||
impl KylesLambda {
|
||||
/// Construct a rolling Kyle's lambda over `window` paired observations.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::InvalidPeriod`] if `window < 2` (the regression
|
||||
/// variance needs at least two observations).
|
||||
pub fn new(window: usize) -> Result<Self> {
|
||||
if window < 2 {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "kyle's lambda needs window >= 2",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
window,
|
||||
prev_mid: None,
|
||||
pairs: VecDeque::with_capacity(window),
|
||||
sum_q: 0.0,
|
||||
sum_dm: 0.0,
|
||||
sum_qq: 0.0,
|
||||
sum_qdm: 0.0,
|
||||
})
|
||||
}
|
||||
|
||||
/// The configured window length, in paired observations.
|
||||
pub const fn window(&self) -> usize {
|
||||
self.window
|
||||
}
|
||||
|
||||
fn push_pair(&mut self, signed_vol: f64, delta_mid: f64) -> Option<f64> {
|
||||
if self.pairs.len() == self.window {
|
||||
let (old_q, old_dm) = self.pairs.pop_front().expect("non-empty");
|
||||
self.sum_q -= old_q;
|
||||
self.sum_dm -= old_dm;
|
||||
self.sum_qq -= old_q * old_q;
|
||||
self.sum_qdm -= old_q * old_dm;
|
||||
}
|
||||
self.pairs.push_back((signed_vol, delta_mid));
|
||||
self.sum_q += signed_vol;
|
||||
self.sum_dm += delta_mid;
|
||||
self.sum_qq += signed_vol * signed_vol;
|
||||
self.sum_qdm += signed_vol * delta_mid;
|
||||
if self.pairs.len() < self.window {
|
||||
return None;
|
||||
}
|
||||
let n = self.window as f64;
|
||||
let mean_q = self.sum_q / n;
|
||||
let mean_dm = self.sum_dm / n;
|
||||
let var_q = (self.sum_qq / n - mean_q * mean_q).max(0.0);
|
||||
let cov = self.sum_qdm / n - mean_q * mean_dm;
|
||||
if var_q == 0.0 {
|
||||
// Constant signed-volume window has no defined slope.
|
||||
return Some(0.0);
|
||||
}
|
||||
Some(cov / var_q)
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for KylesLambda {
|
||||
type Input = TradeQuote;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, quote: TradeQuote) -> Option<f64> {
|
||||
let mid = quote.mid;
|
||||
let signed_vol = quote.trade.size * quote.trade.side.sign();
|
||||
let Some(prev) = self.prev_mid else {
|
||||
self.prev_mid = Some(mid);
|
||||
return None;
|
||||
};
|
||||
self.prev_mid = Some(mid);
|
||||
self.push_pair(signed_vol, mid - prev)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.prev_mid = None;
|
||||
self.pairs.clear();
|
||||
self.sum_q = 0.0;
|
||||
self.sum_dm = 0.0;
|
||||
self.sum_qq = 0.0;
|
||||
self.sum_qdm = 0.0;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.window + 1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.pairs.len() == self.window
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"KylesLambda"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::microstructure::{Side, Trade};
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
fn quotes_with_impact(n: usize, impact: f64) -> Vec<TradeQuote> {
|
||||
let mut mid = 100.0;
|
||||
(0..n)
|
||||
.map(|i| {
|
||||
let side = if i % 2 == 0 { Side::Buy } else { Side::Sell };
|
||||
let size = 1.0 + (i % 3) as f64;
|
||||
let signed = size * side.sign();
|
||||
mid += impact * signed;
|
||||
let trade = Trade::new(mid, size, side, 0).unwrap();
|
||||
TradeQuote::new(trade, mid).unwrap()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_window_below_two() {
|
||||
assert!(KylesLambda::new(0).is_err());
|
||||
assert!(KylesLambda::new(1).is_err());
|
||||
assert!(KylesLambda::new(2).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let kl = KylesLambda::new(14).unwrap();
|
||||
assert_eq!(kl.name(), "KylesLambda");
|
||||
assert_eq!(kl.window(), 14);
|
||||
assert_eq!(kl.warmup_period(), 15);
|
||||
assert!(!kl.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recovers_constant_impact_slope() {
|
||||
// mid moves exactly 0.5 per unit signed volume -> lambda = 0.5.
|
||||
let last = KylesLambda::new(6)
|
||||
.unwrap()
|
||||
.batch("es_with_impact(20, 0.5))
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.last()
|
||||
.unwrap();
|
||||
assert_relative_eq!(last, 0.5, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_impact_reads_negative() {
|
||||
let last = KylesLambda::new(6)
|
||||
.unwrap()
|
||||
.batch("es_with_impact(20, -0.3))
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.last()
|
||||
.unwrap();
|
||||
assert_relative_eq!(last, -0.3, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constant_signed_volume_is_zero() {
|
||||
// Every trade is a buy of size 1: signed volume is constant -> var 0 -> 0.
|
||||
let mut mid = 100.0;
|
||||
let quotes: Vec<TradeQuote> = (0..10)
|
||||
.map(|_| {
|
||||
mid += 0.01;
|
||||
let trade = Trade::new(mid, 1.0, Side::Buy, 0).unwrap();
|
||||
TradeQuote::new(trade, mid).unwrap()
|
||||
})
|
||||
.collect();
|
||||
let last = KylesLambda::new(5)
|
||||
.unwrap()
|
||||
.batch("es)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.last()
|
||||
.unwrap();
|
||||
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warms_up_after_window_plus_one() {
|
||||
let mut kl = KylesLambda::new(3).unwrap();
|
||||
let quotes = quotes_with_impact(4, 0.2);
|
||||
assert_eq!(kl.update(quotes[0]), None); // seeds prev mid
|
||||
assert_eq!(kl.update(quotes[1]), None);
|
||||
assert_eq!(kl.update(quotes[2]), None);
|
||||
assert!(!kl.is_ready());
|
||||
assert!(kl.update(quotes[3]).is_some());
|
||||
assert!(kl.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let quotes = quotes_with_impact(40, 0.15);
|
||||
let batch = KylesLambda::new(10).unwrap().batch("es);
|
||||
let mut kl = KylesLambda::new(10).unwrap();
|
||||
let streamed: Vec<_> = quotes.iter().map(|q| kl.update(*q)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut kl = KylesLambda::new(3).unwrap();
|
||||
for q in quotes_with_impact(6, 0.2) {
|
||||
kl.update(q);
|
||||
}
|
||||
assert!(kl.is_ready());
|
||||
kl.reset();
|
||||
assert!(!kl.is_ready());
|
||||
assert_eq!(kl.update(quotes_with_impact(1, 0.2)[0]), None);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
//! Ladder Bottom candlestick pattern.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Ladder Bottom — a 5-bar bullish reversal. Three long black candles step the
|
||||
/// market down like rungs of a ladder, a fourth black candle finally shows an
|
||||
/// upper shadow (the first sign of buying), and a white candle then gaps up into
|
||||
/// its body to confirm the turn.
|
||||
///
|
||||
/// ```text
|
||||
/// bar1, bar2, bar3 black, with consecutively lower opens AND closes
|
||||
/// bar4 black with an upper shadow (high4 > open4)
|
||||
/// bar5 white, opens above bar4's body (open5 > open4) and closes up
|
||||
/// ```
|
||||
///
|
||||
/// Output is `+1.0` when the pattern completes and `0.0` otherwise. Ladder Bottom
|
||||
/// is a single-direction (bullish-only) reversal, so it never emits `−1.0`. The
|
||||
/// first four bars always return `0.0` because the five-bar window is not yet
|
||||
/// filled. Pattern-shape check only — no trend filter is applied; combine with a
|
||||
/// trend indicator for actionable signals.
|
||||
///
|
||||
/// # Signed ±1 encoding
|
||||
///
|
||||
/// This detector emits the uniform candlestick sign convention shared across the
|
||||
/// pattern family — `+1.0` bullish, `0.0` no pattern — so it drops straight into
|
||||
/// a machine-learning feature matrix as a single dimension.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, LadderBottom};
|
||||
///
|
||||
/// let mut indicator = LadderBottom::new();
|
||||
/// indicator.update(Candle::new(20.0, 20.1, 17.9, 18.0, 1.0, 0).unwrap());
|
||||
/// indicator.update(Candle::new(18.0, 18.1, 15.9, 16.0, 1.0, 1).unwrap());
|
||||
/// indicator.update(Candle::new(16.0, 16.1, 13.9, 14.0, 1.0, 2).unwrap());
|
||||
/// indicator.update(Candle::new(14.0, 15.0, 12.4, 12.5, 1.0, 3).unwrap());
|
||||
/// let out = indicator
|
||||
/// .update(Candle::new(15.0, 17.1, 14.9, 17.0, 1.0, 4).unwrap());
|
||||
/// assert_eq!(out, Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct LadderBottom {
|
||||
c1: Option<Candle>,
|
||||
c2: Option<Candle>,
|
||||
c3: Option<Candle>,
|
||||
c4: Option<Candle>,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl LadderBottom {
|
||||
/// Construct a new Ladder Bottom detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
c1: None,
|
||||
c2: None,
|
||||
c3: None,
|
||||
c4: None,
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for LadderBottom {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let bar1 = self.c1;
|
||||
let bar2 = self.c2;
|
||||
let bar3 = self.c3;
|
||||
let bar4 = self.c4;
|
||||
self.c1 = self.c2;
|
||||
self.c2 = self.c3;
|
||||
self.c3 = self.c4;
|
||||
self.c4 = Some(candle);
|
||||
let (Some(bar1), Some(bar2), Some(bar3), Some(bar4)) = (bar1, bar2, bar3, bar4) else {
|
||||
return Some(0.0);
|
||||
};
|
||||
if bar1.close < bar1.open
|
||||
&& bar2.close < bar2.open
|
||||
&& bar3.close < bar3.open
|
||||
&& bar2.open < bar1.open
|
||||
&& bar2.close < bar1.close
|
||||
&& bar3.open < bar2.open
|
||||
&& bar3.close < bar2.close
|
||||
&& bar4.close < bar4.open
|
||||
&& bar4.high > bar4.open
|
||||
&& candle.close > candle.open
|
||||
&& candle.open > bar4.open
|
||||
{
|
||||
return Some(1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.c1 = None;
|
||||
self.c2 = None;
|
||||
self.c3 = None;
|
||||
self.c4 = None;
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
5
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"LadderBottom"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||
Candle::new(open, high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let t = LadderBottom::new();
|
||||
assert_eq!(t.name(), "LadderBottom");
|
||||
assert_eq!(t.warmup_period(), 5);
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ladder_bottom_is_plus_one() {
|
||||
let mut t = LadderBottom::new();
|
||||
assert_eq!(t.update(c(20.0, 20.1, 17.9, 18.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(18.0, 18.1, 15.9, 16.0, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(16.0, 16.1, 13.9, 14.0, 2)), Some(0.0));
|
||||
assert_eq!(t.update(c(14.0, 15.0, 12.4, 12.5, 3)), Some(0.0));
|
||||
assert_eq!(t.update(c(15.0, 17.1, 14.9, 17.0, 4)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fourth_bar_without_upper_shadow_yields_zero() {
|
||||
let mut t = LadderBottom::new();
|
||||
t.update(c(20.0, 20.1, 17.9, 18.0, 0));
|
||||
t.update(c(18.0, 18.1, 15.9, 16.0, 1));
|
||||
t.update(c(16.0, 16.1, 13.9, 14.0, 2));
|
||||
// bar4 opens at its high -> no upper shadow.
|
||||
t.update(c(14.0, 14.0, 12.4, 12.5, 3));
|
||||
assert_eq!(t.update(c(15.0, 17.1, 14.9, 17.0, 4)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_three_descending_blacks_yields_zero() {
|
||||
let mut t = LadderBottom::new();
|
||||
// bar2 is not lower than bar1.
|
||||
t.update(c(20.0, 20.1, 17.9, 18.0, 0));
|
||||
t.update(c(21.0, 21.1, 18.9, 19.0, 1));
|
||||
t.update(c(16.0, 16.1, 13.9, 14.0, 2));
|
||||
t.update(c(14.0, 15.0, 12.4, 12.5, 3));
|
||||
assert_eq!(t.update(c(15.0, 17.1, 14.9, 17.0, 4)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_four_bars_return_zero() {
|
||||
let mut t = LadderBottom::new();
|
||||
assert_eq!(t.update(c(20.0, 20.1, 17.9, 18.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(18.0, 18.1, 15.9, 16.0, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(16.0, 16.1, 13.9, 14.0, 2)), Some(0.0));
|
||||
assert_eq!(t.update(c(14.0, 15.0, 12.4, 12.5, 3)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 200.0 - i as f64;
|
||||
c(base, base + 0.1, base - 2.1, base - 2.0, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = LadderBottom::new();
|
||||
let mut b = LadderBottom::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = LadderBottom::new();
|
||||
t.update(c(20.0, 20.1, 17.9, 18.0, 0));
|
||||
t.update(c(18.0, 18.1, 15.9, 16.0, 1));
|
||||
t.update(c(16.0, 16.1, 13.9, 14.0, 2));
|
||||
t.update(c(14.0, 15.0, 12.4, 12.5, 3));
|
||||
t.update(c(15.0, 17.1, 14.9, 17.0, 4));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.update(c(20.0, 20.1, 17.9, 18.0, 0)), Some(0.0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
//! Liquidation Features — per-tick long/short liquidation breakdown.
|
||||
|
||||
use crate::derivatives::DerivativesTick;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// The liquidation feature vector emitted by [`LiquidationFeatures`] for one
|
||||
/// tick.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||
pub struct LiquidationFeaturesOutput {
|
||||
/// Long-side liquidation notional on this tick.
|
||||
pub long: f64,
|
||||
/// Short-side liquidation notional on this tick.
|
||||
pub short: f64,
|
||||
/// Net liquidation `long − short` (positive = longs being liquidated).
|
||||
pub net: f64,
|
||||
/// Total liquidation `long + short`.
|
||||
pub total: f64,
|
||||
/// Liquidation imbalance `(long − short) / (long + short)`, in `[−1, +1]`;
|
||||
/// `0.0` when there is no liquidation.
|
||||
pub imbalance: f64,
|
||||
}
|
||||
|
||||
/// Liquidation Features — decomposes the long- and short-side liquidation
|
||||
/// notional carried by each tick into a small feature vector.
|
||||
///
|
||||
/// ```text
|
||||
/// net = longLiquidation − shortLiquidation
|
||||
/// total = longLiquidation + shortLiquidation
|
||||
/// imbalance = net / total (0 when total == 0)
|
||||
/// ```
|
||||
///
|
||||
/// Liquidation cascades are a perpetual-market-specific tail risk: a wave of
|
||||
/// long liquidations forces market sells that beget more liquidations. Splitting
|
||||
/// the flow into net, total and a bounded imbalance turns the raw venue feed
|
||||
/// into model-ready features — `total` sizes the stress, `imbalance` (and its
|
||||
/// sign) says which side is being flushed. A positive imbalance means longs are
|
||||
/// being liquidated (downside cascade), a negative one shorts (upside squeeze).
|
||||
///
|
||||
/// `Input = DerivativesTick`, `Output = LiquidationFeaturesOutput`. Stateless;
|
||||
/// ready after the first tick.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{DerivativesTick, Indicator, LiquidationFeatures};
|
||||
///
|
||||
/// fn tick(long_liq: f64, short_liq: f64) -> DerivativesTick {
|
||||
/// DerivativesTick::new(
|
||||
/// 0.0, 100.0, 100.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, long_liq, short_liq, 0,
|
||||
/// )
|
||||
/// .unwrap()
|
||||
/// }
|
||||
///
|
||||
/// let mut liq = LiquidationFeatures::new();
|
||||
/// // 30 long vs 10 short liquidated: net 20, total 40, imbalance 0.5.
|
||||
/// let out = liq.update(tick(30.0, 10.0)).unwrap();
|
||||
/// assert_eq!(out.net, 20.0);
|
||||
/// assert_eq!(out.total, 40.0);
|
||||
/// assert_eq!(out.imbalance, 0.5);
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct LiquidationFeatures {
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl LiquidationFeatures {
|
||||
/// Construct a new liquidation-features indicator.
|
||||
#[must_use]
|
||||
pub const fn new() -> Self {
|
||||
Self { has_emitted: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for LiquidationFeatures {
|
||||
type Input = DerivativesTick;
|
||||
type Output = LiquidationFeaturesOutput;
|
||||
|
||||
fn update(&mut self, tick: DerivativesTick) -> Option<LiquidationFeaturesOutput> {
|
||||
self.has_emitted = true;
|
||||
let long = tick.long_liquidation;
|
||||
let short = tick.short_liquidation;
|
||||
let net = long - short;
|
||||
let total = long + short;
|
||||
let imbalance = if total == 0.0 { 0.0 } else { net / total };
|
||||
Some(LiquidationFeaturesOutput {
|
||||
long,
|
||||
short,
|
||||
net,
|
||||
total,
|
||||
imbalance,
|
||||
})
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"LiquidationFeatures"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn tick(long_liq: f64, short_liq: f64) -> DerivativesTick {
|
||||
DerivativesTick::new_unchecked(
|
||||
0.0, 100.0, 100.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, long_liq, short_liq, 0,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let liq = LiquidationFeatures::new();
|
||||
assert_eq!(liq.name(), "LiquidationFeatures");
|
||||
assert_eq!(liq.warmup_period(), 1);
|
||||
assert!(!liq.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decomposes_liquidations() {
|
||||
let mut liq = LiquidationFeatures::new();
|
||||
let out = liq.update(tick(30.0, 10.0)).unwrap();
|
||||
assert_eq!(out.long, 30.0);
|
||||
assert_eq!(out.short, 10.0);
|
||||
assert_eq!(out.net, 20.0);
|
||||
assert_eq!(out.total, 40.0);
|
||||
assert_eq!(out.imbalance, 0.5);
|
||||
assert!(liq.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_cascade_is_negative_imbalance() {
|
||||
let mut liq = LiquidationFeatures::new();
|
||||
let out = liq.update(tick(0.0, 50.0)).unwrap();
|
||||
assert_eq!(out.net, -50.0);
|
||||
assert_eq!(out.imbalance, -1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_liquidation_is_zero_imbalance() {
|
||||
let mut liq = LiquidationFeatures::new();
|
||||
let out = liq.update(tick(0.0, 0.0)).unwrap();
|
||||
assert_eq!(out.total, 0.0);
|
||||
assert_eq!(out.imbalance, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let ticks: Vec<DerivativesTick> = (0..20)
|
||||
.map(|i| tick(f64::from(i % 5) * 10.0, f64::from(i % 3) * 10.0))
|
||||
.collect();
|
||||
let mut a = LiquidationFeatures::new();
|
||||
let mut b = LiquidationFeatures::new();
|
||||
assert_eq!(
|
||||
a.batch(&ticks),
|
||||
ticks.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut liq = LiquidationFeatures::new();
|
||||
liq.update(tick(30.0, 10.0));
|
||||
assert!(liq.is_ready());
|
||||
liq.reset();
|
||||
assert!(!liq.is_ready());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
//! Long-Legged Doji candlestick pattern.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Long-Legged Doji — a single-bar indecision signal. A doji with long shadows on
|
||||
/// *both* sides: price ranged widely up and down yet closed essentially where it
|
||||
/// opened, a tug-of-war that often precedes a turn.
|
||||
///
|
||||
/// ```text
|
||||
/// range = high − low
|
||||
/// doji = |close − open| <= 0.1 * range
|
||||
/// long upper = high − max(open, close) >= 0.3 * range
|
||||
/// long lower = min(open, close) − low >= 0.3 * range
|
||||
/// ```
|
||||
///
|
||||
/// Output is `+1.0` when the long-legged doji prints and `0.0` otherwise. This is
|
||||
/// a non-directional indecision flag — it never emits `−1.0` (use
|
||||
/// `DragonflyDoji` / `GravestoneDoji` for the directional single-shadow variants).
|
||||
/// Body and shadow thresholds follow the geometric house style (fixed fractions
|
||||
/// of the bar range) rather than TA-Lib's rolling averages. Pattern-shape check
|
||||
/// only — no trend filter is applied; combine with a trend indicator for
|
||||
/// actionable signals.
|
||||
///
|
||||
/// # Signed ±1 encoding
|
||||
///
|
||||
/// This detector emits the uniform candlestick sign convention shared across the
|
||||
/// pattern family — `+1.0` detected, `0.0` no pattern — so it drops straight into
|
||||
/// a machine-learning feature matrix as a single dimension.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, LongLeggedDoji, Indicator};
|
||||
///
|
||||
/// let mut indicator = LongLeggedDoji::new();
|
||||
/// // Tiny body, long shadows on both sides.
|
||||
/// let candle = Candle::new(10.0, 12.0, 8.0, 10.05, 1.0, 0).unwrap();
|
||||
/// assert_eq!(indicator.update(candle), Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct LongLeggedDoji {
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl LongLeggedDoji {
|
||||
/// Construct a new Long-Legged Doji detector.
|
||||
pub const fn new() -> Self {
|
||||
Self { has_emitted: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for LongLeggedDoji {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let range = candle.high - candle.low;
|
||||
if range <= 0.0 {
|
||||
return Some(0.0);
|
||||
}
|
||||
if (candle.close - candle.open).abs() > 0.1 * range {
|
||||
return Some(0.0);
|
||||
}
|
||||
let upper = candle.high - candle.open.max(candle.close);
|
||||
let lower = candle.open.min(candle.close) - candle.low;
|
||||
if upper >= 0.3 * range && lower >= 0.3 * range {
|
||||
return Some(1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"LongLeggedDoji"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||
Candle::new(open, high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let t = LongLeggedDoji::new();
|
||||
assert_eq!(t.name(), "LongLeggedDoji");
|
||||
assert_eq!(t.warmup_period(), 1);
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_legged_is_plus_one() {
|
||||
let mut t = LongLeggedDoji::new();
|
||||
assert_eq!(t.update(c(10.0, 12.0, 8.0, 10.05, 0)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_sided_shadow_yields_zero() {
|
||||
let mut t = LongLeggedDoji::new();
|
||||
// Dragonfly shape: long lower shadow but no upper -> not long-legged.
|
||||
assert_eq!(t.update(c(10.0, 10.05, 6.0, 10.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_doji_yields_zero() {
|
||||
let mut t = LongLeggedDoji::new();
|
||||
assert_eq!(t.update(c(10.0, 12.0, 8.0, 11.5, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_range_yields_zero() {
|
||||
let mut t = LongLeggedDoji::new();
|
||||
assert_eq!(t.update(c(10.0, 10.0, 10.0, 10.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 100.0 + i as f64;
|
||||
c(base, base + 3.0, base - 3.0, base + 0.05, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = LongLeggedDoji::new();
|
||||
let mut b = LongLeggedDoji::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = LongLeggedDoji::new();
|
||||
t.update(c(10.0, 12.0, 8.0, 10.05, 0));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
//! Long Line candlestick pattern.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
/// Long Line — a single candle whose range is *longer* than the recent average and
|
||||
/// whose body dominates that range (a solid directional bar). Because "long" only
|
||||
/// has meaning relative to recent activity, the detector compares each candle's
|
||||
/// range against a rolling average of the previous `period` ranges.
|
||||
///
|
||||
/// ```text
|
||||
/// avg = mean range of the previous `period` candles
|
||||
/// long line = range > avg AND |close − open| >= 0.5 * range
|
||||
/// white -> +1.0, black -> −1.0
|
||||
/// ```
|
||||
///
|
||||
/// Output is `+1.0` (long white line), `−1.0` (long black line), or `0.0`
|
||||
/// otherwise. The first `period` candles return `0.0` while the rolling average
|
||||
/// fills. `period` defaults to `5` and must be at least `1`. This rolling baseline
|
||||
/// is the one place the family departs from a purely intra-candle rule, since a
|
||||
/// short/long classification is inherently scale-relative. Pattern-shape check
|
||||
/// only — no trend filter is applied; combine with a trend indicator for
|
||||
/// actionable signals.
|
||||
///
|
||||
/// # Signed ±1 encoding
|
||||
///
|
||||
/// This detector emits the uniform candlestick sign convention shared across the
|
||||
/// pattern family — `+1.0` bullish, `−1.0` bearish, `0.0` no pattern — so it
|
||||
/// drops straight into a machine-learning feature matrix as a single dimension.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, LongLine};
|
||||
///
|
||||
/// let mut indicator = LongLine::new();
|
||||
/// // Five quiet bars fill the rolling average.
|
||||
/// for ts in 0..5 {
|
||||
/// indicator.update(Candle::new(10.0, 10.5, 9.5, 10.2, 1.0, ts).unwrap());
|
||||
/// }
|
||||
/// // A wide solid white bar is a long white line.
|
||||
/// let out = indicator
|
||||
/// .update(Candle::new(10.0, 13.0, 9.9, 12.9, 1.0, 5).unwrap());
|
||||
/// assert_eq!(out, Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LongLine {
|
||||
period: usize,
|
||||
ranges: VecDeque<f64>,
|
||||
}
|
||||
|
||||
impl Default for LongLine {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl LongLine {
|
||||
/// Construct a Long Line detector with the default 5-candle rolling average.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
period: 5,
|
||||
ranges: VecDeque::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a Long Line detector with a custom averaging period.
|
||||
///
|
||||
/// `period` must be at least `1`.
|
||||
pub fn with_period(period: usize) -> Result<Self> {
|
||||
if period == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
ranges: VecDeque::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured averaging period.
|
||||
pub fn period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for LongLine {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
let range = candle.high - candle.low;
|
||||
let body = candle.close - candle.open;
|
||||
if self.ranges.len() < self.period {
|
||||
self.ranges.push_back(range);
|
||||
return Some(0.0);
|
||||
}
|
||||
let avg = self.ranges.iter().sum::<f64>() / self.period as f64;
|
||||
self.ranges.push_back(range);
|
||||
self.ranges.pop_front();
|
||||
if range > avg && body.abs() >= 0.5 * range {
|
||||
return Some(if body > 0.0 { 1.0 } else { -1.0 });
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.ranges.clear();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.ranges.len() >= self.period
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"LongLine"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||
Candle::new(open, high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
fn warm(t: &mut LongLine) {
|
||||
for ts in 0..5 {
|
||||
assert_eq!(t.update(c(10.0, 10.5, 9.5, 10.2, ts)), Some(0.0));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_period() {
|
||||
assert!(LongLine::with_period(0).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_valid_period() {
|
||||
let t = LongLine::with_period(10).unwrap();
|
||||
assert_eq!(t.period(), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let t = LongLine::new();
|
||||
assert_eq!(t.name(), "LongLine");
|
||||
assert_eq!(t.warmup_period(), 5);
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.period(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_white_line_is_plus_one() {
|
||||
let mut t = LongLine::new();
|
||||
warm(&mut t);
|
||||
assert!(t.is_ready());
|
||||
assert_eq!(t.update(c(10.0, 13.0, 9.9, 12.9, 5)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_black_line_is_minus_one() {
|
||||
let mut t = LongLine::new();
|
||||
warm(&mut t);
|
||||
assert_eq!(t.update(c(13.0, 13.1, 9.9, 10.0, 5)), Some(-1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_range_yields_zero() {
|
||||
let mut t = LongLine::new();
|
||||
warm(&mut t);
|
||||
// Range no bigger than the average -> not a long line.
|
||||
assert_eq!(t.update(c(10.0, 10.5, 9.5, 10.2, 5)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wide_range_small_body_yields_zero() {
|
||||
let mut t = LongLine::new();
|
||||
warm(&mut t);
|
||||
// Wide range but a tiny body -> a spinning top, not a long line.
|
||||
assert_eq!(t.update(c(10.5, 13.0, 9.9, 10.6, 5)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warmup_returns_zero() {
|
||||
let mut t = LongLine::new();
|
||||
for ts in 0..5 {
|
||||
assert_eq!(t.update(c(10.0, 13.0, 9.9, 12.9, ts)), Some(0.0));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 100.0 + i as f64;
|
||||
if i % 7 == 0 {
|
||||
c(base, base + 4.0, base - 0.1, base + 3.9, i)
|
||||
} else {
|
||||
c(base, base + 0.5, base - 0.5, base + 0.2, i)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let mut a = LongLine::new();
|
||||
let mut b = LongLine::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = LongLine::new();
|
||||
warm(&mut t);
|
||||
t.update(c(10.0, 13.0, 9.9, 12.9, 5));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.update(c(10.0, 13.0, 9.9, 12.9, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_matches_new() {
|
||||
assert_eq!(LongLine::default().period(), LongLine::new().period());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
//! Long/Short Ratio — aggregate long size relative to short size.
|
||||
|
||||
use crate::derivatives::DerivativesTick;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Long/Short Ratio — the aggregate long size divided by the aggregate short
|
||||
/// size carried by each tick.
|
||||
///
|
||||
/// ```text
|
||||
/// longShortRatio = longSize / shortSize
|
||||
/// ```
|
||||
///
|
||||
/// Exchanges publish the long/short account (or position) ratio as a crowd
|
||||
/// positioning gauge: a ratio above `1` means longs outweigh shorts, below `1`
|
||||
/// the reverse. Extremes are a contrarian signal — an overwhelmingly long crowd
|
||||
/// is fuel for a long squeeze. When the short side is zero the ratio is
|
||||
/// undefined and the indicator reports `0.0`.
|
||||
///
|
||||
/// `Input = DerivativesTick`, `Output = f64`. Stateless; ready after the first
|
||||
/// tick.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{DerivativesTick, Indicator, LongShortRatio};
|
||||
///
|
||||
/// fn tick(long: f64, short: f64) -> DerivativesTick {
|
||||
/// DerivativesTick::new(0.0, 100.0, 100.0, 100.0, 0.0, long, short, 0.0, 0.0, 0.0, 0.0, 0)
|
||||
/// .unwrap()
|
||||
/// }
|
||||
///
|
||||
/// let mut lsr = LongShortRatio::new();
|
||||
/// // 600 longs vs 400 shorts -> 1.5.
|
||||
/// assert_eq!(lsr.update(tick(600.0, 400.0)), Some(1.5));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct LongShortRatio {
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl LongShortRatio {
|
||||
/// Construct a new long/short ratio indicator.
|
||||
#[must_use]
|
||||
pub const fn new() -> Self {
|
||||
Self { has_emitted: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for LongShortRatio {
|
||||
type Input = DerivativesTick;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, tick: DerivativesTick) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
if tick.short_size == 0.0 {
|
||||
// No short side to divide by: the ratio is undefined.
|
||||
return Some(0.0);
|
||||
}
|
||||
Some(tick.long_size / tick.short_size)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"LongShortRatio"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn tick(long: f64, short: f64) -> DerivativesTick {
|
||||
DerivativesTick::new_unchecked(
|
||||
0.0, 100.0, 100.0, 100.0, 0.0, long, short, 0.0, 0.0, 0.0, 0.0, 0,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let lsr = LongShortRatio::new();
|
||||
assert_eq!(lsr.name(), "LongShortRatio");
|
||||
assert_eq!(lsr.warmup_period(), 1);
|
||||
assert!(!lsr.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn divides_long_by_short() {
|
||||
let mut lsr = LongShortRatio::new();
|
||||
assert_eq!(lsr.update(tick(600.0, 400.0)), Some(1.5));
|
||||
assert_eq!(lsr.update(tick(400.0, 800.0)), Some(0.5));
|
||||
assert!(lsr.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_short_is_zero() {
|
||||
let mut lsr = LongShortRatio::new();
|
||||
assert_eq!(lsr.update(tick(600.0, 0.0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let ticks: Vec<DerivativesTick> = (0..20)
|
||||
.map(|i| {
|
||||
tick(
|
||||
500.0 + f64::from(i % 5) * 10.0,
|
||||
400.0 + f64::from(i % 3) * 10.0,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let mut a = LongShortRatio::new();
|
||||
let mut b = LongShortRatio::new();
|
||||
assert_eq!(
|
||||
a.batch(&ticks),
|
||||
ticks.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut lsr = LongShortRatio::new();
|
||||
lsr.update(tick(600.0, 400.0));
|
||||
assert!(lsr.is_ready());
|
||||
lsr.reset();
|
||||
assert!(!lsr.is_ready());
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,13 @@ use crate::traits::Indicator;
|
||||
/// `shadow_tolerance` defaults to `0.05` (5 % of the bar range allowed on each
|
||||
/// side) and must lie in `[0, 1)`.
|
||||
///
|
||||
/// # Signed ±1 encoding
|
||||
///
|
||||
/// This detector already emits the uniform candlestick sign convention shared
|
||||
/// across the pattern family — `+1.0` bullish, `−1.0` bearish, `0.0` no
|
||||
/// pattern — so it drops straight into a machine-learning feature matrix where
|
||||
/// the bullish and bearish variants of the pattern occupy a single dimension.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
//! Mat Hold candlestick pattern.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Mat Hold — a 5-bar bullish continuation. A long white candle is followed by a
|
||||
/// brief three-bar pullback that gaps up and then drifts on small bodies *without*
|
||||
/// surrendering much ground, after which a white candle breaks to a new high and
|
||||
/// the uptrend resumes.
|
||||
///
|
||||
/// ```text
|
||||
/// long body = |close − open| >= 0.5 * (high − low)
|
||||
/// bar1 white & long
|
||||
/// bar2 small body gapping up above bar1 (min(o2,c2) > close1)
|
||||
/// bar2, bar3, bar4 each small (|body| <= 0.5 · body1)
|
||||
/// the pullback holds (min low of bars 2..4 > close1 − penetration·body1)
|
||||
/// bar5 white, closing at a new high (close5 > max high of bars 1..4)
|
||||
/// ```
|
||||
///
|
||||
/// Output is `+1.0` when the pattern completes and `0.0` otherwise. Mat Hold is a
|
||||
/// single-direction (bullish-only) continuation, so it never emits `−1.0`. The
|
||||
/// first four bars always return `0.0` because the five-bar window is not yet
|
||||
/// filled. `penetration` is how far the pullback may retrace into the first body;
|
||||
/// it defaults to `0.5` (TA-Lib's `CDLMATHOLD` default) and must lie in `[0, 1)`.
|
||||
/// Body thresholds follow the geometric house style rather than TA-Lib's rolling
|
||||
/// averages. Pattern-shape check only — no trend filter is applied; combine with a
|
||||
/// trend indicator for actionable signals.
|
||||
///
|
||||
/// # Signed ±1 encoding
|
||||
///
|
||||
/// This detector emits the uniform candlestick sign convention shared across the
|
||||
/// pattern family — `+1.0` bullish, `0.0` no pattern — so it drops straight into
|
||||
/// a machine-learning feature matrix as a single dimension.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, MatHold};
|
||||
///
|
||||
/// let mut indicator = MatHold::new();
|
||||
/// indicator.update(Candle::new(10.0, 15.1, 9.9, 15.0, 1.0, 0).unwrap());
|
||||
/// indicator.update(Candle::new(16.0, 16.1, 15.4, 15.5, 1.0, 1).unwrap());
|
||||
/// indicator.update(Candle::new(15.5, 15.6, 14.9, 15.0, 1.0, 2).unwrap());
|
||||
/// indicator.update(Candle::new(15.0, 15.1, 14.4, 14.5, 1.0, 3).unwrap());
|
||||
/// let out = indicator
|
||||
/// .update(Candle::new(14.5, 17.1, 14.4, 17.0, 1.0, 4).unwrap());
|
||||
/// assert_eq!(out, Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MatHold {
|
||||
penetration: f64,
|
||||
c1: Option<Candle>,
|
||||
c2: Option<Candle>,
|
||||
c3: Option<Candle>,
|
||||
c4: Option<Candle>,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl Default for MatHold {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl MatHold {
|
||||
/// Construct a Mat Hold detector with the default 0.5 penetration.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
penetration: 0.5,
|
||||
c1: None,
|
||||
c2: None,
|
||||
c3: None,
|
||||
c4: None,
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a Mat Hold detector with a custom penetration fraction.
|
||||
///
|
||||
/// `penetration` must lie in `[0, 1)`.
|
||||
pub fn with_penetration(penetration: f64) -> Result<Self> {
|
||||
if !(0.0..1.0).contains(&penetration) {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "mat hold penetration must lie in [0, 1)",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
penetration,
|
||||
c1: None,
|
||||
c2: None,
|
||||
c3: None,
|
||||
c4: None,
|
||||
has_emitted: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured penetration fraction.
|
||||
pub fn penetration(&self) -> f64 {
|
||||
self.penetration
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for MatHold {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let bar1 = self.c1;
|
||||
let bar2 = self.c2;
|
||||
let bar3 = self.c3;
|
||||
let bar4 = self.c4;
|
||||
self.c1 = self.c2;
|
||||
self.c2 = self.c3;
|
||||
self.c3 = self.c4;
|
||||
self.c4 = Some(candle);
|
||||
let (Some(bar1), Some(bar2), Some(bar3), Some(bar4)) = (bar1, bar2, bar3, bar4) else {
|
||||
return Some(0.0);
|
||||
};
|
||||
let range1 = bar1.high - bar1.low;
|
||||
if range1 <= 0.0 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let body1 = bar1.close - bar1.open;
|
||||
if body1 < 0.5 * range1 {
|
||||
return Some(0.0); // bar1 must be a long white body
|
||||
}
|
||||
let small = 0.5 * body1;
|
||||
if (bar2.close - bar2.open).abs() > small
|
||||
|| (bar3.close - bar3.open).abs() > small
|
||||
|| (bar4.close - bar4.open).abs() > small
|
||||
{
|
||||
return Some(0.0); // the three pullback bars must be small
|
||||
}
|
||||
// bar2 gaps up above bar1's body.
|
||||
if bar2.open.min(bar2.close) <= bar1.close {
|
||||
return Some(0.0);
|
||||
}
|
||||
// The pullback must hold above the penetration line.
|
||||
let hold_line = bar1.close - self.penetration * body1;
|
||||
if bar2.low.min(bar3.low).min(bar4.low) <= hold_line {
|
||||
return Some(0.0);
|
||||
}
|
||||
// bar5 breaks to a new high on a white body.
|
||||
let max_high = bar1.high.max(bar2.high).max(bar3.high).max(bar4.high);
|
||||
if candle.close > candle.open && candle.close > max_high {
|
||||
return Some(1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.c1 = None;
|
||||
self.c2 = None;
|
||||
self.c3 = None;
|
||||
self.c4 = None;
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
5
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"MatHold"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||
Candle::new(open, high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_penetration() {
|
||||
assert!(MatHold::with_penetration(-0.01).is_err());
|
||||
assert!(MatHold::with_penetration(1.0).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_valid_penetration() {
|
||||
let t = MatHold::with_penetration(0.3).unwrap();
|
||||
assert!((t.penetration() - 0.3).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let t = MatHold::default();
|
||||
assert_eq!(t.name(), "MatHold");
|
||||
assert_eq!(t.warmup_period(), 5);
|
||||
assert!(!t.is_ready());
|
||||
assert!((t.penetration() - 0.5).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mat_hold_is_plus_one() {
|
||||
let mut t = MatHold::new();
|
||||
assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(16.0, 16.1, 15.4, 15.5, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(15.5, 15.6, 14.9, 15.0, 2)), Some(0.0));
|
||||
assert_eq!(t.update(c(15.0, 15.1, 14.4, 14.5, 3)), Some(0.0));
|
||||
assert_eq!(t.update(c(14.5, 17.1, 14.4, 17.0, 4)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pullback_breaks_hold_yields_zero() {
|
||||
let mut t = MatHold::new();
|
||||
t.update(c(10.0, 15.1, 9.9, 15.0, 0));
|
||||
t.update(c(16.0, 16.1, 15.4, 15.5, 1));
|
||||
t.update(c(15.5, 15.6, 14.9, 15.0, 2));
|
||||
// bar4 dips below the hold line (close1 - 0.5*body1 = 12.5).
|
||||
t.update(c(13.0, 13.1, 12.0, 12.4, 3));
|
||||
assert_eq!(t.update(c(14.5, 17.1, 12.0, 17.0, 4)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_new_high_yields_zero() {
|
||||
let mut t = MatHold::new();
|
||||
t.update(c(10.0, 15.1, 9.9, 15.0, 0));
|
||||
t.update(c(16.0, 16.1, 15.4, 15.5, 1));
|
||||
t.update(c(15.5, 15.6, 14.9, 15.0, 2));
|
||||
t.update(c(15.0, 15.1, 14.4, 14.5, 3));
|
||||
// bar5 white but closes below the prior max high (16.1).
|
||||
assert_eq!(t.update(c(14.5, 16.0, 14.4, 15.9, 4)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_four_bars_return_zero() {
|
||||
let mut t = MatHold::new();
|
||||
assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(16.0, 16.1, 15.4, 15.5, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(15.5, 15.6, 14.9, 15.0, 2)), Some(0.0));
|
||||
assert_eq!(t.update(c(15.0, 15.1, 14.4, 14.5, 3)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 100.0 + i as f64;
|
||||
c(base, base + 5.2, base - 0.1, base + 5.0, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = MatHold::new();
|
||||
let mut b = MatHold::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = MatHold::new();
|
||||
t.update(c(10.0, 15.1, 9.9, 15.0, 0));
|
||||
t.update(c(16.0, 16.1, 15.4, 15.5, 1));
|
||||
t.update(c(15.5, 15.6, 14.9, 15.0, 2));
|
||||
t.update(c(15.0, 15.1, 14.4, 14.5, 3));
|
||||
t.update(c(14.5, 17.1, 14.4, 17.0, 4));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_range_first_bar_yields_zero() {
|
||||
let mut t = MatHold::new();
|
||||
// Flat first bar (range1 == 0) -> rejected.
|
||||
t.update(c(10.0, 10.0, 10.0, 10.0, 0));
|
||||
t.update(c(16.0, 16.1, 15.4, 15.5, 1));
|
||||
t.update(c(15.5, 15.6, 14.9, 15.0, 2));
|
||||
t.update(c(15.0, 15.1, 14.4, 14.5, 3));
|
||||
assert_eq!(t.update(c(14.5, 17.1, 14.4, 17.0, 4)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_first_body_yields_zero() {
|
||||
let mut t = MatHold::new();
|
||||
// bar1 has a wide range but a tiny body -> not a long white body.
|
||||
t.update(c(10.0, 16.0, 9.0, 10.5, 0));
|
||||
t.update(c(16.0, 16.1, 15.4, 15.5, 1));
|
||||
t.update(c(15.5, 15.6, 14.9, 15.0, 2));
|
||||
t.update(c(15.0, 15.1, 14.4, 14.5, 3));
|
||||
assert_eq!(t.update(c(14.5, 17.1, 14.4, 17.0, 4)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_gap_up_yields_zero() {
|
||||
let mut t = MatHold::new();
|
||||
// Long white bar1 with small pullbacks, but bar2 fails to gap up above
|
||||
// bar1's close.
|
||||
t.update(c(10.0, 15.1, 9.9, 15.0, 0));
|
||||
t.update(c(14.5, 14.7, 14.3, 14.5, 1));
|
||||
t.update(c(14.5, 14.7, 14.3, 14.6, 2));
|
||||
t.update(c(14.6, 14.8, 14.4, 14.7, 3));
|
||||
assert_eq!(t.update(c(14.7, 17.1, 14.6, 17.0, 4)), Some(0.0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
//! Matching Low candlestick pattern.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Matching Low — a 2-bar bullish reversal. Two black candles in a decline close
|
||||
/// at the *same* level: the second sell-off cannot push price any lower, so the
|
||||
/// matching closes mark a support floor.
|
||||
///
|
||||
/// ```text
|
||||
/// bar1, bar2 both black
|
||||
/// equal closes = |close2 − close1| <= 0.05 · mean(range1, range2)
|
||||
/// ```
|
||||
///
|
||||
/// Output is `+1.0` when the pattern completes and `0.0` otherwise. Matching Low
|
||||
/// is a single-direction (bullish-only) reversal, so it never emits `−1.0`. The
|
||||
/// first bar always returns `0.0` because the two-bar window is not yet filled.
|
||||
/// The close-equality tolerance follows the geometric house style rather than
|
||||
/// TA-Lib's rolling averages. Pattern-shape check only — no trend filter is
|
||||
/// applied; combine with a trend indicator for actionable signals.
|
||||
///
|
||||
/// # Signed ±1 encoding
|
||||
///
|
||||
/// This detector emits the uniform candlestick sign convention shared across the
|
||||
/// pattern family — `+1.0` bullish, `0.0` no pattern — so it drops straight into
|
||||
/// a machine-learning feature matrix as a single dimension.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, MatchingLow};
|
||||
///
|
||||
/// let mut indicator = MatchingLow::new();
|
||||
/// indicator.update(Candle::new(15.0, 15.1, 9.9, 10.0, 1.0, 0).unwrap());
|
||||
/// let out = indicator
|
||||
/// .update(Candle::new(13.0, 13.1, 9.9, 10.0, 1.0, 1).unwrap());
|
||||
/// assert_eq!(out, Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct MatchingLow {
|
||||
prev: Option<Candle>,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl MatchingLow {
|
||||
/// Construct a new Matching Low detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
prev: None,
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for MatchingLow {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let prev = self.prev;
|
||||
self.prev = Some(candle);
|
||||
let Some(bar1) = prev else {
|
||||
return Some(0.0);
|
||||
};
|
||||
let mean_range = 0.5 * ((bar1.high - bar1.low) + (candle.high - candle.low));
|
||||
let tol = 0.05 * mean_range;
|
||||
if bar1.close < bar1.open
|
||||
&& candle.close < candle.open
|
||||
&& (candle.close - bar1.close).abs() <= tol
|
||||
{
|
||||
return Some(1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.prev = None;
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
2
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"MatchingLow"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||
Candle::new(open, high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let t = MatchingLow::new();
|
||||
assert_eq!(t.name(), "MatchingLow");
|
||||
assert_eq!(t.warmup_period(), 2);
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matching_low_is_plus_one() {
|
||||
let mut t = MatchingLow::new();
|
||||
assert_eq!(t.update(c(15.0, 15.1, 9.9, 10.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(13.0, 13.1, 9.9, 10.0, 1)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn different_close_yields_zero() {
|
||||
let mut t = MatchingLow::new();
|
||||
t.update(c(15.0, 15.1, 9.9, 10.0, 0));
|
||||
// Second close well away from the first.
|
||||
assert_eq!(t.update(c(13.0, 13.1, 11.4, 11.5, 1)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn second_bar_white_yields_zero() {
|
||||
let mut t = MatchingLow::new();
|
||||
t.update(c(15.0, 15.1, 9.9, 10.0, 0));
|
||||
assert_eq!(t.update(c(9.0, 10.1, 8.9, 10.0, 1)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_bar_returns_zero() {
|
||||
let mut t = MatchingLow::new();
|
||||
assert_eq!(t.update(c(15.0, 15.1, 9.9, 10.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 100.0 - i as f64;
|
||||
c(base + 2.0, base + 2.1, base - 0.1, base, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = MatchingLow::new();
|
||||
let mut b = MatchingLow::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = MatchingLow::new();
|
||||
t.update(c(15.0, 15.1, 9.9, 10.0, 0));
|
||||
t.update(c(13.0, 13.1, 9.9, 10.0, 1));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.update(c(15.0, 15.1, 9.9, 10.0, 0)), Some(0.0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
//! Microprice — size-weighted fair value of the top of book.
|
||||
|
||||
use crate::microstructure::OrderBook;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Microprice — the size-weighted mid of the top of book.
|
||||
///
|
||||
/// The microprice tilts the mid toward the side that is *more likely to be
|
||||
/// hit*: it weights each touch price by the size resting on the **opposite**
|
||||
/// side, so a heavy ask (sell pressure) pulls the fair value down toward the
|
||||
/// bid, and vice versa:
|
||||
///
|
||||
/// ```text
|
||||
/// microprice = (bidPrice₁·askSize₁ + askPrice₁·bidSize₁) / (bidSize₁ + askSize₁)
|
||||
/// ```
|
||||
///
|
||||
/// When both top sizes are zero the weighting is undefined and the plain mid
|
||||
/// `(bidPrice₁ + askPrice₁) / 2` is returned. An empty book yields `0`.
|
||||
///
|
||||
/// `Input = OrderBook`, `Output = f64`. Stateless; ready after the first
|
||||
/// snapshot.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Indicator, Level, Microprice, OrderBook};
|
||||
///
|
||||
/// let book = OrderBook::new(
|
||||
/// vec![Level::new(100.0, 1.0).unwrap()],
|
||||
/// vec![Level::new(101.0, 3.0).unwrap()],
|
||||
/// )
|
||||
/// .unwrap();
|
||||
/// let mut mp = Microprice::new();
|
||||
/// // (100·3 + 101·1) / (1 + 3) = 401 / 4 = 100.25 — pulled toward the bid.
|
||||
/// assert_eq!(mp.update(book), Some(100.25));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Microprice {
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl Microprice {
|
||||
/// Construct a new microprice indicator.
|
||||
pub const fn new() -> Self {
|
||||
Self { has_emitted: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for Microprice {
|
||||
type Input = OrderBook;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, book: OrderBook) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let (Some(bid), Some(ask)) = (book.best_bid(), book.best_ask()) else {
|
||||
return Some(0.0);
|
||||
};
|
||||
let total = bid.size + ask.size;
|
||||
if total <= 0.0 {
|
||||
return Some(f64::midpoint(bid.price, ask.price));
|
||||
}
|
||||
Some((bid.price * ask.size + ask.price * bid.size) / total)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"Microprice"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::microstructure::Level;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn book(bids: &[(f64, f64)], asks: &[(f64, f64)]) -> OrderBook {
|
||||
let to_levels = |xs: &[(f64, f64)]| {
|
||||
xs.iter()
|
||||
.map(|&(p, s)| Level::new(p, s).unwrap())
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
OrderBook::new(to_levels(bids), to_levels(asks)).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let mp = Microprice::new();
|
||||
assert_eq!(mp.name(), "Microprice");
|
||||
assert_eq!(mp.warmup_period(), 1);
|
||||
assert!(!mp.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn weights_toward_thin_side() {
|
||||
let mut mp = Microprice::new();
|
||||
// Heavy ask -> microprice pulled toward bid.
|
||||
assert_eq!(
|
||||
mp.update(book(&[(100.0, 1.0)], &[(101.0, 3.0)])),
|
||||
Some(100.25)
|
||||
);
|
||||
assert!(mp.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn balanced_top_equals_mid() {
|
||||
let mut mp = Microprice::new();
|
||||
assert_eq!(
|
||||
mp.update(book(&[(100.0, 2.0)], &[(101.0, 2.0)])),
|
||||
Some(100.5)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_size_falls_back_to_mid() {
|
||||
let mut mp = Microprice::new();
|
||||
assert_eq!(
|
||||
mp.update(book(&[(100.0, 0.0)], &[(102.0, 0.0)])),
|
||||
Some(101.0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_book_is_zero() {
|
||||
let mut mp = Microprice::new();
|
||||
assert_eq!(
|
||||
mp.update(OrderBook::new_unchecked(vec![], vec![])),
|
||||
Some(0.0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let books: Vec<OrderBook> = (0..20)
|
||||
.map(|i| {
|
||||
let ask = 1.0 + f64::from(i % 4);
|
||||
book(&[(100.0, 2.0)], &[(101.0, ask)])
|
||||
})
|
||||
.collect();
|
||||
let mut a = Microprice::new();
|
||||
let mut b = Microprice::new();
|
||||
assert_eq!(
|
||||
a.batch(&books),
|
||||
books
|
||||
.iter()
|
||||
.map(|x| b.update(x.clone()))
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut mp = Microprice::new();
|
||||
mp.update(book(&[(100.0, 1.0)], &[(101.0, 1.0)]));
|
||||
assert!(mp.is_ready());
|
||||
mp.reset();
|
||||
assert!(!mp.is_ready());
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,13 @@
|
||||
//! [`FAMILIES`]. Every public name is re-exported flat from this module and
|
||||
//! from the crate root for convenience.
|
||||
|
||||
mod abandoned_baby;
|
||||
mod acceleration_bands;
|
||||
mod accelerator_oscillator;
|
||||
mod ad_oscillator;
|
||||
mod adaptive_cycle;
|
||||
mod adl;
|
||||
mod advance_block;
|
||||
mod adx;
|
||||
mod adxr;
|
||||
mod alligator;
|
||||
@@ -26,9 +28,12 @@ mod average_drawdown;
|
||||
mod awesome_oscillator;
|
||||
mod awesome_oscillator_histogram;
|
||||
mod balance_of_power;
|
||||
mod belt_hold;
|
||||
mod beta;
|
||||
mod bollinger;
|
||||
mod bollinger_bandwidth;
|
||||
mod breakaway;
|
||||
mod calendar_spread;
|
||||
mod calmar_ratio;
|
||||
mod camarilla_pivots;
|
||||
mod cci;
|
||||
@@ -40,52 +45,76 @@ mod chande_kroll_stop;
|
||||
mod chandelier_exit;
|
||||
mod choppiness_index;
|
||||
mod classic_pivots;
|
||||
mod closing_marubozu;
|
||||
mod cmf;
|
||||
mod cmo;
|
||||
mod coefficient_of_variation;
|
||||
mod cointegration;
|
||||
mod concealing_baby_swallow;
|
||||
mod conditional_value_at_risk;
|
||||
mod connors_rsi;
|
||||
mod coppock;
|
||||
mod counterattack;
|
||||
mod cvd;
|
||||
mod cybernetic_cycle;
|
||||
mod decycler;
|
||||
mod decycler_oscillator;
|
||||
mod dema;
|
||||
mod demand_index;
|
||||
mod demark_pivots;
|
||||
mod depth_slope;
|
||||
mod detrended_std_dev;
|
||||
mod doji;
|
||||
mod doji_star;
|
||||
mod donchian;
|
||||
mod donchian_stop;
|
||||
mod double_bollinger;
|
||||
mod downside_gap_three_methods;
|
||||
mod dpo;
|
||||
mod dragonfly_doji;
|
||||
mod drawdown_duration;
|
||||
mod ease_of_movement;
|
||||
mod effective_spread;
|
||||
mod ehlers_stochastic;
|
||||
mod elder_impulse;
|
||||
mod ema;
|
||||
mod empirical_mode_decomposition;
|
||||
mod engulfing;
|
||||
mod evening_doji_star;
|
||||
mod evwma;
|
||||
mod falling_three_methods;
|
||||
mod fama;
|
||||
mod fibonacci_pivots;
|
||||
mod fisher_transform;
|
||||
mod footprint;
|
||||
mod force_index;
|
||||
mod fractal_chaos_bands;
|
||||
mod frama;
|
||||
mod funding_basis;
|
||||
mod funding_rate;
|
||||
mod funding_rate_mean;
|
||||
mod funding_rate_zscore;
|
||||
mod gain_loss_ratio;
|
||||
mod gap_side_by_side_white;
|
||||
mod garman_klass;
|
||||
mod gravestone_doji;
|
||||
mod hammer;
|
||||
mod hanging_man;
|
||||
mod harami;
|
||||
mod heikin_ashi;
|
||||
mod high_wave;
|
||||
mod hikkake;
|
||||
mod hikkake_modified;
|
||||
mod hilbert_dominant_cycle;
|
||||
mod hilo_activator;
|
||||
mod historical_volatility;
|
||||
mod hma;
|
||||
mod homing_pigeon;
|
||||
mod hurst_channel;
|
||||
mod hurst_exponent;
|
||||
mod ichimoku;
|
||||
mod identical_three_crows;
|
||||
mod in_neck;
|
||||
mod inertia;
|
||||
mod information_ratio;
|
||||
mod initial_balance;
|
||||
@@ -96,32 +125,52 @@ mod jma;
|
||||
mod kama;
|
||||
mod kelly_criterion;
|
||||
mod keltner;
|
||||
mod kicking;
|
||||
mod kicking_by_length;
|
||||
mod kst;
|
||||
mod kurtosis;
|
||||
mod kvo;
|
||||
mod kyles_lambda;
|
||||
mod ladder_bottom;
|
||||
mod laguerre_rsi;
|
||||
mod lead_lag_cross_correlation;
|
||||
mod linreg;
|
||||
mod linreg_angle;
|
||||
mod linreg_channel;
|
||||
mod linreg_slope;
|
||||
mod liquidation_features;
|
||||
mod long_legged_doji;
|
||||
mod long_line;
|
||||
mod long_short_ratio;
|
||||
mod ma_envelope;
|
||||
mod macd;
|
||||
mod mama;
|
||||
mod market_facilitation_index;
|
||||
mod marubozu;
|
||||
mod mass_index;
|
||||
mod mat_hold;
|
||||
mod matching_low;
|
||||
mod max_drawdown;
|
||||
mod mcginley_dynamic;
|
||||
mod median_absolute_deviation;
|
||||
mod median_price;
|
||||
mod mfi;
|
||||
mod microprice;
|
||||
mod mom;
|
||||
mod morning_doji_star;
|
||||
mod morning_evening_star;
|
||||
mod natr;
|
||||
mod nvi;
|
||||
mod ob_imbalance_full;
|
||||
mod ob_imbalance_top1;
|
||||
mod ob_imbalance_topn;
|
||||
mod obv;
|
||||
mod oi_delta;
|
||||
mod oi_price_divergence;
|
||||
mod oi_weighted;
|
||||
mod omega_ratio;
|
||||
mod on_neck;
|
||||
mod opening_marubozu;
|
||||
mod opening_range;
|
||||
mod pain_index;
|
||||
mod pair_spread_zscore;
|
||||
@@ -137,10 +186,14 @@ mod ppo;
|
||||
mod profit_factor;
|
||||
mod psar;
|
||||
mod pvi;
|
||||
mod quoted_spread;
|
||||
mod r_squared;
|
||||
mod realized_spread;
|
||||
mod recovery_factor;
|
||||
mod relative_strength_ab;
|
||||
mod renko_trailing_stop;
|
||||
mod rickshaw_man;
|
||||
mod rising_three_methods;
|
||||
mod roc;
|
||||
mod rogers_satchell;
|
||||
mod roofing_filter;
|
||||
@@ -148,8 +201,11 @@ mod rsi;
|
||||
mod rvi;
|
||||
mod rvi_volatility;
|
||||
mod rwi;
|
||||
mod separating_lines;
|
||||
mod sharpe_ratio;
|
||||
mod shooting_star;
|
||||
mod short_line;
|
||||
mod signed_volume;
|
||||
mod sine_wave;
|
||||
mod skewness;
|
||||
mod sma;
|
||||
@@ -158,17 +214,22 @@ mod smma;
|
||||
mod sortino_ratio;
|
||||
mod spearman_correlation;
|
||||
mod spinning_top;
|
||||
mod stalled_pattern;
|
||||
mod standard_error;
|
||||
mod standard_error_bands;
|
||||
mod starc_bands;
|
||||
mod stc;
|
||||
mod std_dev;
|
||||
mod step_trailing_stop;
|
||||
mod stick_sandwich;
|
||||
mod stoch_rsi;
|
||||
mod stochastic;
|
||||
mod super_smoother;
|
||||
mod super_trend;
|
||||
mod t3;
|
||||
mod taker_buy_sell_ratio;
|
||||
mod takuri;
|
||||
mod tasuki_gap;
|
||||
mod td_combo;
|
||||
mod td_countdown;
|
||||
mod td_demarker;
|
||||
@@ -182,10 +243,15 @@ mod td_risk_level;
|
||||
mod td_sequential;
|
||||
mod td_setup;
|
||||
mod tema;
|
||||
mod term_structure_basis;
|
||||
mod three_inside;
|
||||
mod three_line_strike;
|
||||
mod three_outside;
|
||||
mod three_soldiers_or_crows;
|
||||
mod three_stars_in_south;
|
||||
mod thrusting;
|
||||
mod tii;
|
||||
mod trade_imbalance;
|
||||
mod treynor_ratio;
|
||||
mod trima;
|
||||
mod trix;
|
||||
@@ -194,9 +260,13 @@ mod tsi;
|
||||
mod tsv;
|
||||
mod ttm_squeeze;
|
||||
mod tweezer;
|
||||
mod two_crows;
|
||||
mod typical_price;
|
||||
mod ulcer_index;
|
||||
mod ultimate_oscillator;
|
||||
mod unique_three_river;
|
||||
mod upside_gap_three_methods;
|
||||
mod upside_gap_two_crows;
|
||||
mod value_area;
|
||||
mod value_at_risk;
|
||||
mod variance;
|
||||
@@ -223,11 +293,13 @@ mod zero_lag_macd;
|
||||
mod zig_zag;
|
||||
mod zlema;
|
||||
|
||||
pub use abandoned_baby::AbandonedBaby;
|
||||
pub use acceleration_bands::{AccelerationBands, AccelerationBandsOutput};
|
||||
pub use accelerator_oscillator::AcceleratorOscillator;
|
||||
pub use ad_oscillator::AdOscillator;
|
||||
pub use adaptive_cycle::AdaptiveCycle;
|
||||
pub use adl::Adl;
|
||||
pub use advance_block::AdvanceBlock;
|
||||
pub use adx::{Adx, AdxOutput};
|
||||
pub use adxr::Adxr;
|
||||
pub use alligator::{Alligator, AlligatorOutput};
|
||||
@@ -245,9 +317,12 @@ pub use average_drawdown::AverageDrawdown;
|
||||
pub use awesome_oscillator::AwesomeOscillator;
|
||||
pub use awesome_oscillator_histogram::AwesomeOscillatorHistogram;
|
||||
pub use balance_of_power::BalanceOfPower;
|
||||
pub use belt_hold::BeltHold;
|
||||
pub use beta::Beta;
|
||||
pub use bollinger::{BollingerBands, BollingerOutput};
|
||||
pub use bollinger_bandwidth::BollingerBandwidth;
|
||||
pub use breakaway::Breakaway;
|
||||
pub use calendar_spread::CalendarSpread;
|
||||
pub use calmar_ratio::CalmarRatio;
|
||||
pub use camarilla_pivots::{Camarilla, CamarillaPivotsOutput};
|
||||
pub use cci::Cci;
|
||||
@@ -259,52 +334,76 @@ pub use chande_kroll_stop::{ChandeKrollStop, ChandeKrollStopOutput};
|
||||
pub use chandelier_exit::{ChandelierExit, ChandelierExitOutput};
|
||||
pub use choppiness_index::ChoppinessIndex;
|
||||
pub use classic_pivots::{ClassicPivots, ClassicPivotsOutput};
|
||||
pub use closing_marubozu::ClosingMarubozu;
|
||||
pub use cmf::ChaikinMoneyFlow;
|
||||
pub use cmo::Cmo;
|
||||
pub use coefficient_of_variation::CoefficientOfVariation;
|
||||
pub use cointegration::{Cointegration, CointegrationOutput};
|
||||
pub use concealing_baby_swallow::ConcealingBabySwallow;
|
||||
pub use conditional_value_at_risk::ConditionalValueAtRisk;
|
||||
pub use connors_rsi::ConnorsRsi;
|
||||
pub use coppock::Coppock;
|
||||
pub use counterattack::Counterattack;
|
||||
pub use cvd::CumulativeVolumeDelta;
|
||||
pub use cybernetic_cycle::CyberneticCycle;
|
||||
pub use decycler::Decycler;
|
||||
pub use decycler_oscillator::DecyclerOscillator;
|
||||
pub use dema::Dema;
|
||||
pub use demand_index::DemandIndex;
|
||||
pub use demark_pivots::{DemarkPivots, DemarkPivotsOutput};
|
||||
pub use depth_slope::DepthSlope;
|
||||
pub use detrended_std_dev::DetrendedStdDev;
|
||||
pub use doji::Doji;
|
||||
pub use doji_star::DojiStar;
|
||||
pub use donchian::{Donchian, DonchianOutput};
|
||||
pub use donchian_stop::{DonchianStop, DonchianStopOutput};
|
||||
pub use double_bollinger::{DoubleBollinger, DoubleBollingerOutput};
|
||||
pub use downside_gap_three_methods::DownsideGapThreeMethods;
|
||||
pub use dpo::Dpo;
|
||||
pub use dragonfly_doji::DragonflyDoji;
|
||||
pub use drawdown_duration::DrawdownDuration;
|
||||
pub use ease_of_movement::EaseOfMovement;
|
||||
pub use effective_spread::EffectiveSpread;
|
||||
pub use ehlers_stochastic::EhlersStochastic;
|
||||
pub use elder_impulse::ElderImpulse;
|
||||
pub use ema::Ema;
|
||||
pub use empirical_mode_decomposition::EmpiricalModeDecomposition;
|
||||
pub use engulfing::Engulfing;
|
||||
pub use evening_doji_star::EveningDojiStar;
|
||||
pub use evwma::Evwma;
|
||||
pub use falling_three_methods::FallingThreeMethods;
|
||||
pub use fama::Fama;
|
||||
pub use fibonacci_pivots::{FibonacciPivots, FibonacciPivotsOutput};
|
||||
pub use fisher_transform::FisherTransform;
|
||||
pub use footprint::{Footprint, FootprintLevel, FootprintOutput};
|
||||
pub use force_index::ForceIndex;
|
||||
pub use fractal_chaos_bands::{FractalChaosBands, FractalChaosBandsOutput};
|
||||
pub use frama::Frama;
|
||||
pub use funding_basis::FundingBasis;
|
||||
pub use funding_rate::FundingRate;
|
||||
pub use funding_rate_mean::FundingRateMean;
|
||||
pub use funding_rate_zscore::FundingRateZScore;
|
||||
pub use gain_loss_ratio::GainLossRatio;
|
||||
pub use gap_side_by_side_white::GapSideBySideWhite;
|
||||
pub use garman_klass::GarmanKlassVolatility;
|
||||
pub use gravestone_doji::GravestoneDoji;
|
||||
pub use hammer::Hammer;
|
||||
pub use hanging_man::HangingMan;
|
||||
pub use harami::Harami;
|
||||
pub use heikin_ashi::{HeikinAshi, HeikinAshiOutput};
|
||||
pub use high_wave::HighWave;
|
||||
pub use hikkake::Hikkake;
|
||||
pub use hikkake_modified::HikkakeModified;
|
||||
pub use hilbert_dominant_cycle::HilbertDominantCycle;
|
||||
pub use hilo_activator::HiLoActivator;
|
||||
pub use historical_volatility::HistoricalVolatility;
|
||||
pub use hma::Hma;
|
||||
pub use homing_pigeon::HomingPigeon;
|
||||
pub use hurst_channel::{HurstChannel, HurstChannelOutput};
|
||||
pub use hurst_exponent::HurstExponent;
|
||||
pub use ichimoku::{Ichimoku, IchimokuOutput};
|
||||
pub use identical_three_crows::IdenticalThreeCrows;
|
||||
pub use in_neck::InNeck;
|
||||
pub use inertia::Inertia;
|
||||
pub use information_ratio::InformationRatio;
|
||||
pub use initial_balance::{InitialBalance, InitialBalanceOutput};
|
||||
@@ -315,32 +414,52 @@ pub use jma::Jma;
|
||||
pub use kama::Kama;
|
||||
pub use kelly_criterion::KellyCriterion;
|
||||
pub use keltner::{Keltner, KeltnerOutput};
|
||||
pub use kicking::Kicking;
|
||||
pub use kicking_by_length::KickingByLength;
|
||||
pub use kst::{Kst, KstOutput};
|
||||
pub use kurtosis::Kurtosis;
|
||||
pub use kvo::Kvo;
|
||||
pub use kyles_lambda::KylesLambda;
|
||||
pub use ladder_bottom::LadderBottom;
|
||||
pub use laguerre_rsi::LaguerreRsi;
|
||||
pub use lead_lag_cross_correlation::{LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput};
|
||||
pub use linreg::LinearRegression;
|
||||
pub use linreg_angle::LinRegAngle;
|
||||
pub use linreg_channel::{LinRegChannel, LinRegChannelOutput};
|
||||
pub use linreg_slope::LinRegSlope;
|
||||
pub use liquidation_features::{LiquidationFeatures, LiquidationFeaturesOutput};
|
||||
pub use long_legged_doji::LongLeggedDoji;
|
||||
pub use long_line::LongLine;
|
||||
pub use long_short_ratio::LongShortRatio;
|
||||
pub use ma_envelope::{MaEnvelope, MaEnvelopeOutput};
|
||||
pub use macd::{MacdIndicator, MacdOutput};
|
||||
pub use mama::{Mama, MamaOutput};
|
||||
pub use market_facilitation_index::MarketFacilitationIndex;
|
||||
pub use marubozu::Marubozu;
|
||||
pub use mass_index::MassIndex;
|
||||
pub use mat_hold::MatHold;
|
||||
pub use matching_low::MatchingLow;
|
||||
pub use max_drawdown::MaxDrawdown;
|
||||
pub use mcginley_dynamic::McGinleyDynamic;
|
||||
pub use median_absolute_deviation::MedianAbsoluteDeviation;
|
||||
pub use median_price::MedianPrice;
|
||||
pub use mfi::Mfi;
|
||||
pub use microprice::Microprice;
|
||||
pub use mom::Mom;
|
||||
pub use morning_doji_star::MorningDojiStar;
|
||||
pub use morning_evening_star::MorningEveningStar;
|
||||
pub use natr::Natr;
|
||||
pub use nvi::Nvi;
|
||||
pub use ob_imbalance_full::OrderBookImbalanceFull;
|
||||
pub use ob_imbalance_top1::OrderBookImbalanceTop1;
|
||||
pub use ob_imbalance_topn::OrderBookImbalanceTopN;
|
||||
pub use obv::Obv;
|
||||
pub use oi_delta::OpenInterestDelta;
|
||||
pub use oi_price_divergence::OIPriceDivergence;
|
||||
pub use oi_weighted::OIWeighted;
|
||||
pub use omega_ratio::OmegaRatio;
|
||||
pub use on_neck::OnNeck;
|
||||
pub use opening_marubozu::OpeningMarubozu;
|
||||
pub use opening_range::{OpeningRange, OpeningRangeOutput};
|
||||
pub use pain_index::PainIndex;
|
||||
pub use pair_spread_zscore::PairSpreadZScore;
|
||||
@@ -356,10 +475,14 @@ pub use ppo::Ppo;
|
||||
pub use profit_factor::ProfitFactor;
|
||||
pub use psar::Psar;
|
||||
pub use pvi::Pvi;
|
||||
pub use quoted_spread::QuotedSpread;
|
||||
pub use r_squared::RSquared;
|
||||
pub use realized_spread::RealizedSpread;
|
||||
pub use recovery_factor::RecoveryFactor;
|
||||
pub use relative_strength_ab::{RelativeStrengthAB, RelativeStrengthOutput};
|
||||
pub use renko_trailing_stop::RenkoTrailingStop;
|
||||
pub use rickshaw_man::RickshawMan;
|
||||
pub use rising_three_methods::RisingThreeMethods;
|
||||
pub use roc::Roc;
|
||||
pub use rogers_satchell::RogersSatchellVolatility;
|
||||
pub use roofing_filter::RoofingFilter;
|
||||
@@ -367,8 +490,11 @@ pub use rsi::Rsi;
|
||||
pub use rvi::Rvi;
|
||||
pub use rvi_volatility::RviVolatility;
|
||||
pub use rwi::{Rwi, RwiOutput};
|
||||
pub use separating_lines::SeparatingLines;
|
||||
pub use sharpe_ratio::SharpeRatio;
|
||||
pub use shooting_star::ShootingStar;
|
||||
pub use short_line::ShortLine;
|
||||
pub use signed_volume::SignedVolume;
|
||||
pub use sine_wave::SineWave;
|
||||
pub use skewness::Skewness;
|
||||
pub use sma::Sma;
|
||||
@@ -377,17 +503,22 @@ pub use smma::Smma;
|
||||
pub use sortino_ratio::SortinoRatio;
|
||||
pub use spearman_correlation::SpearmanCorrelation;
|
||||
pub use spinning_top::SpinningTop;
|
||||
pub use stalled_pattern::StalledPattern;
|
||||
pub use standard_error::StandardError;
|
||||
pub use standard_error_bands::{StandardErrorBands, StandardErrorBandsOutput};
|
||||
pub use starc_bands::{StarcBands, StarcBandsOutput};
|
||||
pub use stc::Stc;
|
||||
pub use std_dev::StdDev;
|
||||
pub use step_trailing_stop::StepTrailingStop;
|
||||
pub use stick_sandwich::StickSandwich;
|
||||
pub use stoch_rsi::StochRsi;
|
||||
pub use stochastic::{Stochastic, StochasticOutput};
|
||||
pub use super_smoother::SuperSmoother;
|
||||
pub use super_trend::{SuperTrend, SuperTrendOutput};
|
||||
pub use t3::T3;
|
||||
pub use taker_buy_sell_ratio::TakerBuySellRatio;
|
||||
pub use takuri::Takuri;
|
||||
pub use tasuki_gap::TasukiGap;
|
||||
pub use td_combo::TdCombo;
|
||||
pub use td_countdown::TdCountdown;
|
||||
pub use td_demarker::TdDeMarker;
|
||||
@@ -401,10 +532,15 @@ pub use td_risk_level::{TdRiskLevel, TdRiskLevelOutput};
|
||||
pub use td_sequential::{TdSequential, TdSequentialOutput};
|
||||
pub use td_setup::TdSetup;
|
||||
pub use tema::Tema;
|
||||
pub use term_structure_basis::TermStructureBasis;
|
||||
pub use three_inside::ThreeInside;
|
||||
pub use three_line_strike::ThreeLineStrike;
|
||||
pub use three_outside::ThreeOutside;
|
||||
pub use three_soldiers_or_crows::ThreeSoldiersOrCrows;
|
||||
pub use three_stars_in_south::ThreeStarsInSouth;
|
||||
pub use thrusting::Thrusting;
|
||||
pub use tii::Tii;
|
||||
pub use trade_imbalance::TradeImbalance;
|
||||
pub use treynor_ratio::TreynorRatio;
|
||||
pub use trima::Trima;
|
||||
pub use trix::Trix;
|
||||
@@ -413,9 +549,13 @@ pub use tsi::Tsi;
|
||||
pub use tsv::Tsv;
|
||||
pub use ttm_squeeze::{TtmSqueeze, TtmSqueezeOutput};
|
||||
pub use tweezer::Tweezer;
|
||||
pub use two_crows::TwoCrows;
|
||||
pub use typical_price::TypicalPrice;
|
||||
pub use ulcer_index::UlcerIndex;
|
||||
pub use ultimate_oscillator::UltimateOscillator;
|
||||
pub use unique_three_river::UniqueThreeRiver;
|
||||
pub use upside_gap_three_methods::UpsideGapThreeMethods;
|
||||
pub use upside_gap_two_crows::UpsideGapTwoCrows;
|
||||
pub use value_area::{ValueArea, ValueAreaOutput};
|
||||
pub use value_at_risk::ValueAtRisk;
|
||||
pub use variance::Variance;
|
||||
@@ -705,6 +845,86 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
|
||||
"SpinningTop",
|
||||
"ThreeInside",
|
||||
"ThreeOutside",
|
||||
"TwoCrows",
|
||||
"UpsideGapTwoCrows",
|
||||
"IdenticalThreeCrows",
|
||||
"ThreeLineStrike",
|
||||
"ThreeStarsInSouth",
|
||||
"AbandonedBaby",
|
||||
"AdvanceBlock",
|
||||
"BeltHold",
|
||||
"Breakaway",
|
||||
"Counterattack",
|
||||
"DojiStar",
|
||||
"DragonflyDoji",
|
||||
"GravestoneDoji",
|
||||
"LongLeggedDoji",
|
||||
"RickshawMan",
|
||||
"EveningDojiStar",
|
||||
"MorningDojiStar",
|
||||
"GapSideBySideWhite",
|
||||
"HighWave",
|
||||
"Hikkake",
|
||||
"HikkakeModified",
|
||||
"HomingPigeon",
|
||||
"OnNeck",
|
||||
"InNeck",
|
||||
"Thrusting",
|
||||
"SeparatingLines",
|
||||
"Kicking",
|
||||
"KickingByLength",
|
||||
"LadderBottom",
|
||||
"MatHold",
|
||||
"MatchingLow",
|
||||
"LongLine",
|
||||
"ShortLine",
|
||||
"RisingThreeMethods",
|
||||
"FallingThreeMethods",
|
||||
"UpsideGapThreeMethods",
|
||||
"DownsideGapThreeMethods",
|
||||
"StalledPattern",
|
||||
"StickSandwich",
|
||||
"Takuri",
|
||||
"ClosingMarubozu",
|
||||
"OpeningMarubozu",
|
||||
"TasukiGap",
|
||||
"UniqueThreeRiver",
|
||||
"ConcealingBabySwallow",
|
||||
],
|
||||
),
|
||||
(
|
||||
"Microstructure",
|
||||
&[
|
||||
"OrderBookImbalanceTop1",
|
||||
"OrderBookImbalanceTopN",
|
||||
"OrderBookImbalanceFull",
|
||||
"Microprice",
|
||||
"QuotedSpread",
|
||||
"DepthSlope",
|
||||
"SignedVolume",
|
||||
"CumulativeVolumeDelta",
|
||||
"TradeImbalance",
|
||||
"EffectiveSpread",
|
||||
"RealizedSpread",
|
||||
"KylesLambda",
|
||||
"Footprint",
|
||||
],
|
||||
),
|
||||
(
|
||||
"Derivatives",
|
||||
&[
|
||||
"FundingRate",
|
||||
"FundingRateMean",
|
||||
"FundingRateZScore",
|
||||
"FundingBasis",
|
||||
"OpenInterestDelta",
|
||||
"OIPriceDivergence",
|
||||
"OIWeighted",
|
||||
"LongShortRatio",
|
||||
"TakerBuySellRatio",
|
||||
"LiquidationFeatures",
|
||||
"TermStructureBasis",
|
||||
"CalendarSpread",
|
||||
],
|
||||
),
|
||||
(
|
||||
@@ -761,6 +981,6 @@ mod family_tests {
|
||||
// the actual indicator count is the early-warning signal that an
|
||||
// indicator was added without being assigned a family.
|
||||
let total: usize = FAMILIES.iter().map(|(_, ns)| ns.len()).sum();
|
||||
assert_eq!(total, 214, "FAMILIES total drifted from indicator count");
|
||||
assert_eq!(total, 284, "FAMILIES total drifted from indicator count");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
//! Morning Doji Star candlestick pattern.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Morning Doji Star — a 3-bar bullish bottom reversal. A long black bar extends
|
||||
/// the decline, a doji gaps down below it (the star of indecision), then a white
|
||||
/// bar gaps back up and closes deep into the first body, confirming the turn.
|
||||
///
|
||||
/// ```text
|
||||
/// long body = |close − open| >= 0.5 * (high − low)
|
||||
/// doji = |close − open| <= 0.1 * (high − low)
|
||||
/// bar1 black & long
|
||||
/// bar2 doji, body gaps DOWN below bar1 body (max(o2,c2) < close1)
|
||||
/// bar3 white, body gaps UP above the doji (min(o3,c3) > max(o2,c2))
|
||||
/// bar3 closes deep into bar1 body (close3 > close1 + penetration·body1)
|
||||
/// ```
|
||||
///
|
||||
/// Output is `+1.0` when the pattern completes and `0.0` otherwise. Morning Doji
|
||||
/// Star is a single-direction (bullish-only) reversal, so it never emits `−1.0`.
|
||||
/// The first two bars always return `0.0` because the three-bar window is not yet
|
||||
/// filled. `penetration` is how far into the first body the third bar must close;
|
||||
/// it defaults to `0.3` (TA-Lib's `CDLMORNINGDOJISTAR` default) and must lie in
|
||||
/// `[0, 1)`. Body and doji thresholds follow the geometric house style rather than
|
||||
/// TA-Lib's rolling averages. Pattern-shape check only — no trend filter is
|
||||
/// applied; combine with a trend indicator for actionable signals.
|
||||
///
|
||||
/// # Signed ±1 encoding
|
||||
///
|
||||
/// This detector emits the uniform candlestick sign convention shared across the
|
||||
/// pattern family — `+1.0` bullish, `0.0` no pattern — so it drops straight into
|
||||
/// a machine-learning feature matrix as a single dimension.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, MorningDojiStar};
|
||||
///
|
||||
/// let mut indicator = MorningDojiStar::new();
|
||||
/// indicator.update(Candle::new(15.0, 15.1, 9.9, 10.0, 1.0, 0).unwrap());
|
||||
/// indicator.update(Candle::new(8.0, 8.1, 7.9, 8.0, 1.0, 1).unwrap());
|
||||
/// let out = indicator
|
||||
/// .update(Candle::new(9.0, 13.1, 8.9, 13.0, 1.0, 2).unwrap());
|
||||
/// assert_eq!(out, Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MorningDojiStar {
|
||||
penetration: f64,
|
||||
prev: Option<Candle>,
|
||||
prev_prev: Option<Candle>,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl Default for MorningDojiStar {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl MorningDojiStar {
|
||||
/// Construct a Morning Doji Star detector with the default 0.3 penetration.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
penetration: 0.3,
|
||||
prev: None,
|
||||
prev_prev: None,
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a Morning Doji Star detector with a custom penetration fraction.
|
||||
///
|
||||
/// `penetration` must lie in `[0, 1)`.
|
||||
pub fn with_penetration(penetration: f64) -> Result<Self> {
|
||||
if !(0.0..1.0).contains(&penetration) {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "morning doji star penetration must lie in [0, 1)",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
penetration,
|
||||
prev: None,
|
||||
prev_prev: None,
|
||||
has_emitted: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured penetration fraction.
|
||||
pub fn penetration(&self) -> f64 {
|
||||
self.penetration
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for MorningDojiStar {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let bar1 = self.prev_prev;
|
||||
let bar2 = self.prev;
|
||||
self.prev_prev = self.prev;
|
||||
self.prev = Some(candle);
|
||||
let (Some(bar1), Some(bar2)) = (bar1, bar2) else {
|
||||
return Some(0.0);
|
||||
};
|
||||
let range1 = bar1.high - bar1.low;
|
||||
let range2 = bar2.high - bar2.low;
|
||||
if range1 <= 0.0 || range2 <= 0.0 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let body1 = bar1.open - bar1.close;
|
||||
if body1 < 0.5 * range1 {
|
||||
return Some(0.0); // bar1 must be a long black body
|
||||
}
|
||||
if (bar2.close - bar2.open).abs() > 0.1 * range2 {
|
||||
return Some(0.0); // bar2 must be a doji
|
||||
}
|
||||
let star_top = bar2.open.max(bar2.close);
|
||||
let bar3_bottom = candle.open.min(candle.close);
|
||||
if star_top < bar1.close
|
||||
&& candle.close > candle.open
|
||||
&& bar3_bottom > star_top
|
||||
&& candle.close > bar1.close + self.penetration * body1
|
||||
{
|
||||
return Some(1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.prev = None;
|
||||
self.prev_prev = None;
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
3
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"MorningDojiStar"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||
Candle::new(open, high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_penetration() {
|
||||
assert!(MorningDojiStar::with_penetration(-0.01).is_err());
|
||||
assert!(MorningDojiStar::with_penetration(1.0).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_valid_penetration() {
|
||||
let t = MorningDojiStar::with_penetration(0.5).unwrap();
|
||||
assert!((t.penetration() - 0.5).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let t = MorningDojiStar::default();
|
||||
assert_eq!(t.name(), "MorningDojiStar");
|
||||
assert_eq!(t.warmup_period(), 3);
|
||||
assert!(!t.is_ready());
|
||||
assert!((t.penetration() - 0.3).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn morning_doji_star_is_plus_one() {
|
||||
let mut t = MorningDojiStar::new();
|
||||
assert_eq!(t.update(c(15.0, 15.1, 9.9, 10.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(8.0, 8.1, 7.9, 8.0, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(9.0, 13.1, 8.9, 13.0, 2)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn middle_not_doji_yields_zero() {
|
||||
let mut t = MorningDojiStar::new();
|
||||
t.update(c(15.0, 15.1, 9.9, 10.0, 0));
|
||||
// Wide-bodied star, not a doji.
|
||||
t.update(c(8.0, 10.1, 7.9, 10.0, 1));
|
||||
assert_eq!(t.update(c(9.0, 13.1, 8.9, 13.0, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shallow_close_yields_zero() {
|
||||
let mut t = MorningDojiStar::new();
|
||||
t.update(c(15.0, 15.1, 9.9, 10.0, 0));
|
||||
t.update(c(8.0, 8.1, 7.9, 8.0, 1));
|
||||
// bar3 white but closes at 11.0 -> only 1.0 into the 5.0 body (< 0.3·5).
|
||||
assert_eq!(t.update(c(9.0, 11.1, 8.9, 11.0, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_two_bars_return_zero() {
|
||||
let mut t = MorningDojiStar::new();
|
||||
assert_eq!(t.update(c(15.0, 15.1, 9.9, 10.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(8.0, 8.1, 7.9, 8.0, 1)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 100.0 + i as f64;
|
||||
c(base + 5.0, base + 5.1, base - 0.1, base, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = MorningDojiStar::new();
|
||||
let mut b = MorningDojiStar::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = MorningDojiStar::new();
|
||||
t.update(c(15.0, 15.1, 9.9, 10.0, 0));
|
||||
t.update(c(8.0, 8.1, 7.9, 8.0, 1));
|
||||
t.update(c(9.0, 13.1, 8.9, 13.0, 2));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.update(c(15.0, 15.1, 9.9, 10.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_range_yields_zero() {
|
||||
let mut t = MorningDojiStar::new();
|
||||
// Flat first bar (range1 == 0) -> rejected.
|
||||
t.update(c(10.0, 10.0, 10.0, 10.0, 0));
|
||||
t.update(c(8.0, 8.1, 7.9, 8.0, 1));
|
||||
assert_eq!(t.update(c(9.0, 13.1, 8.9, 13.0, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_first_body_yields_zero() {
|
||||
let mut t = MorningDojiStar::new();
|
||||
// bar1 has a wide range but a tiny body -> not a long black body.
|
||||
t.update(c(10.0, 16.0, 9.0, 9.8, 0));
|
||||
t.update(c(8.0, 8.1, 7.9, 8.0, 1));
|
||||
assert_eq!(t.update(c(9.0, 13.1, 8.9, 13.0, 2)), Some(0.0));
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user