diff --git a/.env b/.env deleted file mode 100644 index ee97059..0000000 --- a/.env +++ /dev/null @@ -1,2 +0,0 @@ -POLYGON_BASE_URL=https://api.polygon.io -POLYGON_TOKEN=2MALlIG_J_IsFKLlEGZ_SYCD_99REgkSStEc9O diff --git a/.gitignore b/.gitignore index a727c0a..02c0bb7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ +.env /target /data +/out diff --git a/Cargo.lock b/Cargo.lock index c9aca8a..66cbfa1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + [[package]] name = "anyhow" version = "1.0.44" @@ -35,14 +44,28 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" name = "backtester" version = "0.1.0" dependencies = [ + "anyhow", "async-trait", + "bdays", "chrono", + "chrono-tz", + "csv", + "dotenv", + "futures", + "indicatif 0.16.2", + "lazy_static", "num-traits", - "polygon 0.11.2", + "polygon", + "rand", "rust_decimal", "serde", "serde_json", + "serde_with", + "stream-flatten-iters", "thiserror", + "tokio", + "tracing", + "tracing-subscriber", "uuid", ] @@ -52,12 +75,33 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "bdays" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310d1621c71e83fd1908b71178a4ab1fb4439c893af3f724bd03de0ad35ed8fa" +dependencies = [ + "chrono", +] + [[package]] name = "bitflags" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "lazy_static", + "memchr", + "regex-automata", + "serde", +] + [[package]] name = "bumpalo" version = "3.6.1" @@ -96,6 +140,42 @@ dependencies = [ "winapi", ] +[[package]] +name = "chrono-tz" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64c01c1c607d25c71bbaa67c113d6c6b36c434744b4fd66691d711b5b1bc0c8b" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", + "serde", +] + +[[package]] +name = "chrono-tz-build" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db058d493fb2f65f41861bfed7e3fe6335264a9f0f92710cab5bdf01fef09069" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + +[[package]] +name = "console" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28b32d32ca44b70c3e4acd7db1babf555fa026e385fb95f18028f88848b3c31" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "terminal_size", + "winapi", +] + [[package]] name = "core-foundation" version = "0.9.1" @@ -112,6 +192,63 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" +[[package]] +name = "csv" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" +dependencies = [ + "bstr", + "csv-core", + "itoa 0.4.7", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" +dependencies = [ + "memchr", +] + +[[package]] +name = "darling" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "757c0ded2af11d8e739c4daea1ac623dd1624b06c844cf3f5a39f1bdbd99bb12" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c34d8efb62d0c2d7f60ece80f75e5c63c1588ba68032740494b0b9a996466e3" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade7bff147130fe5e6d39f089c6bd49ec0250f35d70b2eebf72afdfc919f15cc" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "dotenv" version = "0.15.0" @@ -124,6 +261,12 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding_rs" version = "0.8.28" @@ -166,9 +309,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.15" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7e43a803dae2fa37c1f6a8fe121e1f7bf9548b4dfc0522a42f34145dadfc27" +checksum = "8cd0210d8c325c245ff06fd95a3b13689a1a276ac8cfa8e8720cb840bfb84b9e" dependencies = [ "futures-channel", "futures-core", @@ -181,9 +324,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.15" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e682a68b29a882df0545c143dc3646daefe80ba479bcdede94d5a703de2871e2" +checksum = "7fc8cd39e3dbf865f7340dce6a2d401d24fd37c6fe6c4f0ee0de8bfca2252d27" dependencies = [ "futures-core", "futures-sink", @@ -191,15 +334,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.15" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0402f765d8a89a26043b889b26ce3c4679d268fa6bb22cd7c6aad98340e179d1" +checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445" [[package]] name = "futures-executor" -version = "0.3.15" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "badaa6a909fac9e7236d0620a2f57f7664640c56575b71a7552fbd68deafab79" +checksum = "7b808bf53348a36cab739d7e04755909b9fcaaa69b7d7e588b37b6ec62704c97" dependencies = [ "futures-core", "futures-task", @@ -208,18 +351,16 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.15" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acc499defb3b348f8d8f3f66415835a9131856ff7714bf10dadfc4ec4bdb29a1" +checksum = "e481354db6b5c353246ccf6a728b0c5511d752c08da7260546fc0933869daa11" [[package]] name = "futures-macro" -version = "0.3.15" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c40298486cdf52cc00cd6d6987892ba502c7656a16a4192a9992b1ccedd121" +checksum = "a89f17b21645bc4ed773c69af9c9a0effd4a3f1a3876eadd453469f8854e7fdd" dependencies = [ - "autocfg", - "proc-macro-hack", "proc-macro2", "quote", "syn", @@ -227,23 +368,22 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.15" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a57bead0ceff0d6dde8f465ecd96c9338121bb7717d3e7b108059531870c4282" +checksum = "996c6442437b62d21a32cd9906f9c41e7dc1e19a9579843fad948696769305af" [[package]] name = "futures-task" -version = "0.3.15" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a16bef9fc1a4dddb5bee51c989e3fbba26569cbb0e31f5b303c184e3dd33dae" +checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12" [[package]] name = "futures-util" -version = "0.3.15" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feb5c238d27e2bf94ffdfd27b2c29e3df4a68c4193bb6427384259e2bf191967" +checksum = "41d22213122356472061ac0f1ab2cee28d2bac8491410fd68c2af53d1cedb83e" dependencies = [ - "autocfg", "futures-channel", "futures-core", "futures-io", @@ -253,8 +393,6 @@ dependencies = [ "memchr", "pin-project-lite", "pin-utils", - "proc-macro-hack", - "proc-macro-nested", "slab", ] @@ -311,7 +449,7 @@ checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11" dependencies = [ "bytes", "fnv", - "itoa", + "itoa 0.4.7", ] [[package]] @@ -352,8 +490,8 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa", - "pin-project", + "itoa 0.4.7", + "pin-project 1.0.7", "socket2", "tokio", "tower-service", @@ -374,6 +512,12 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.2.3" @@ -395,6 +539,30 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "indicatif" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d207dc617c7a380ab07ff572a6e52fa202a2a8f355860ac9c38e23f8196be1b" +dependencies = [ + "console", + "lazy_static", + "number_prefix", + "regex", +] + +[[package]] +name = "indicatif" +version = "0.17.0-beta.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "500f7e5a63596852b9bf7583fe86f9ad08e0df9b4eb58d12e9729071cb4952ca" +dependencies = [ + "console", + "lazy_static", + "number_prefix", + "regex", +] + [[package]] name = "ipnet" version = "2.3.0" @@ -403,9 +571,9 @@ checksum = "47be2f14c678be2fdcab04ab1171db51b2762ce6f0a8ee87c8dd4a04ed216135" [[package]] name = "itertools" -version = "0.10.0" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d572918e350e82412fe766d24b15e6682fb2ed2bbe018280caa810397cb319" +checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" dependencies = [ "either", ] @@ -416,6 +584,12 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + [[package]] name = "js-sys" version = "0.3.51" @@ -446,6 +620,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "matchers" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f099785f7595cc4b4553a174ce30dd7589ef93391ff414dbb67f62392b9e0ce1" +dependencies = [ + "regex-automata", +] + [[package]] name = "matches" version = "0.1.8" @@ -542,6 +725,12 @@ dependencies = [ "libc", ] +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + [[package]] name = "once_cell" version = "1.7.2" @@ -581,19 +770,87 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "parse-zoneinfo" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" +dependencies = [ + "regex", +] + [[package]] name = "percent-encoding" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +[[package]] +name = "phf" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9fc3db1018c4b59d7d582a739436478b6035138b6aecbce989fc91c3e98409f" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", + "uncased", +] + +[[package]] +name = "pin-project" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918192b5c59119d51e0cd221f4d49dde9112824ba717369e903c97d076083d0f" +dependencies = [ + "pin-project-internal 0.4.28", +] + [[package]] name = "pin-project" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7509cc106041c40a4518d2af7a61530e1eed0e6285296a3d8c5472806ccc4a4" dependencies = [ - "pin-project-internal", + "pin-project-internal 1.0.7", +] + +[[package]] +name = "pin-project-internal" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be26700300be6d9d23264c73211d8190e755b6b5ca7a1b28230025511b52a5e" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -627,31 +884,21 @@ checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" [[package]] name = "polygon" -version = "0.1.0" -dependencies = [ - "anyhow", - "backtester", - "chrono", - "dotenv", - "rust_decimal", - "tokio", -] - -[[package]] -name = "polygon" -version = "0.11.2" -source = "git+ssh://git@github.com/Overmuse/polygon?tag=v0.11.2#05dcf3b31d7ab371c0def92d35516c92d298a3c1" +version = "0.14.0" +source = "git+ssh://git@github.com/Overmuse/polygon?tag=v0.14.0#e9c5d464a0bf3cfa76b32aee361a037300fdcab6" dependencies = [ "chrono", + "chrono-tz", "futures", "itertools", - "reqwest", "rust_decimal", "serde", "serde_json", "serde_repr", "thiserror", "tracing", + "uuid", + "vila", ] [[package]] @@ -660,18 +907,6 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" -[[package]] -name = "proc-macro-hack" -version = "0.5.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" - -[[package]] -name = "proc-macro-nested" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" - [[package]] name = "proc-macro2" version = "1.0.26" @@ -692,9 +927,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" dependencies = [ "libc", "rand_chacha", @@ -739,6 +974,30 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + [[package]] name = "remove_dir_all" version = "0.5.3" @@ -794,6 +1053,12 @@ dependencies = [ "serde", ] +[[package]] +name = "rustversion" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088" + [[package]] name = "ryu" version = "1.0.5" @@ -855,11 +1120,11 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.68" +version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" +checksum = "bcbd0344bc6533bc7ec56df11d42fb70f1b912351c0825ccb7211b59d8af7cf5" dependencies = [ - "itoa", + "itoa 1.0.1", "ryu", "serde", ] @@ -882,17 +1147,62 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" dependencies = [ "form_urlencoded", - "itoa", + "itoa 0.4.7", "ryu", "serde", ] +[[package]] +name = "serde_with" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad6056b4cb69b6e43e3a0f055def223380baecc99da683884f205bf347f7c4b3" +dependencies = [ + "chrono", + "rustversion", + "serde", + "serde_with_macros", +] + +[[package]] +name = "serde_with_macros" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12e47be9471c72889ebafb5e14d5ff930d89ae7a67bbdb5f8abb564f845a927e" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "siphasher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533494a8f9b724d33625ab53c6c4800f7cc445895924a8ef649222dcb76e938b" + [[package]] name = "slab" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527" +[[package]] +name = "smallvec" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" + [[package]] name = "socket2" version = "0.4.0" @@ -903,6 +1213,23 @@ dependencies = [ "winapi", ] +[[package]] +name = "stream-flatten-iters" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b10cec45f0cea2f2a8a05c44ea3f400994b29baf8e1690cdd722e25bacd3128" +dependencies = [ + "futures-core", + "pin-project 0.4.28", + "pin-utils", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "syn" version = "1.0.72" @@ -928,6 +1255,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "thiserror" version = "1.0.30" @@ -948,6 +1285,15 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd" +dependencies = [ + "once_cell", +] + [[package]] name = "time" version = "0.1.43" @@ -975,9 +1321,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.6.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd3076b5c8cc18138b8f8814895c11eb4de37114a5d127bafdc5e55798ceef37" +checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144" dependencies = [ "autocfg", "bytes", @@ -987,13 +1333,14 @@ dependencies = [ "num_cpus", "pin-project-lite", "tokio-macros", + "winapi", ] [[package]] name = "tokio-macros" -version = "1.4.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "154794c8f499c2619acd19e839294703e9e32e7630ef5f46ea80d4ef0fbee5eb" +checksum = "c9efc1aba077437943f7515666aa2b882dfabfbfdf89c819ea75a8d6e9eaba5e" dependencies = [ "proc-macro2", "quote", @@ -1032,9 +1379,9 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" [[package]] name = "tracing" -version = "0.1.26" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09adeb8c97449311ccd28a427f96fb563e7fd31aabf994189879d9da2394b89d" +checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105" dependencies = [ "cfg-if", "pin-project-lite", @@ -1044,9 +1391,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.15" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42e6fa53307c8a17e4ccd4dc81cf5ec38db9209f59b222210375b54ee40d1e2" +checksum = "f4f480b8f81512e825f337ad51e94c1eb5d3bbdf2b363dcd01e2b19a9ffe3f8e" dependencies = [ "proc-macro2", "quote", @@ -1055,11 +1402,54 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.18" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "tracing-log" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6923477a48e41c1951f1999ef8bb5a3023eb723ceadafe78ffb65dc366761e3" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb65ea441fbb84f9f6748fd496cf7f63ec9af5bca94dd86456978d055e8eb28b" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9ff14f98b1a4b289c6248a023c1c2fa1491062964e9fed67ab29c4e4da4a052" +checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71" dependencies = [ + "ansi_term", + "chrono", "lazy_static", + "matchers", + "regex", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", ] [[package]] @@ -1068,6 +1458,15 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +[[package]] +name = "uncased" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baeed7327e25054889b9bd4f975f32e5f4c5d434042d59ab6cd4142c0a76ed0" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.5" @@ -1111,6 +1510,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ "getrandom", + "serde", ] [[package]] @@ -1119,6 +1519,27 @@ version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbdbff6266a24120518560b5dc983096efb98462e51d0d68169895b237be3e5d" +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + +[[package]] +name = "vila" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0ecf209339f2228cd168dc85807e222a820d8f4246a85d263ba578ed82dab7" +dependencies = [ + "futures", + "indicatif 0.17.0-beta.1", + "log", + "reqwest", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "want" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index 6d7e996..ed03ec2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,3 @@ -[workspace] -members = [".", "examples/polygon"] -default-members = [".", "examples/polygon"] - [package] name = "backtester" version = "0.1.0" @@ -12,14 +8,31 @@ edition = "2018" [dependencies] async-trait = "0.1" -chrono = "0.4" -num-traits = "0.2.14" -polygon = { git = "ssh://git@github.com/Overmuse/polygon", tag = "v0.11.2", default-features = false, features = ["rest"], optional=true } -rust_decimal = "1.16.0" -serde = "1.0" -serde_json = "1.0" -thiserror = "1.0.30" -uuid = { version = "0.8.2", features = ["v4"] } +bdays = "0.1" +chrono = { version = "0.4", features = ["serde"] } +chrono-tz = "0.6" +csv = "1.1.6" +futures = { version = "0.3"} +indicatif = "0.16" +lazy_static = "1.4" +num-traits = "0.2" +rust_decimal = "1.16" +polygon = { git = "ssh://git@github.com/Overmuse/polygon", tag = "v0.14.0", default-features = false, features = ["rest"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0.73" +serde_with = { version = "1.11", features = ["chrono"] } +stream-flatten-iters = { version = "0.2" } +thiserror = "1.0" +tokio = { version = "1.0", features = ["sync"] } +tracing = "0.1.29" +uuid = { version = "0.8", features = ["v4", "serde"] } + +[[example]] +name = "random_trades" -[features] -default = [] +[dev-dependencies] +anyhow = "1.0" +dotenv = "0.15" +rand = "0.8" +tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } +tracing-subscriber = "0.2" diff --git a/examples/polygon/Cargo.lock b/examples/polygon/Cargo.lock deleted file mode 100644 index 91990f9..0000000 --- a/examples/polygon/Cargo.lock +++ /dev/null @@ -1,1161 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "arrayvec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" - -[[package]] -name = "async-trait" -version = "0.1.51" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "autocfg" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" - -[[package]] -name = "backtester" -version = "0.1.0" -dependencies = [ - "async-trait", - "chrono", - "polygon", - "rust_decimal", - "serde", - "serde_json", -] - -[[package]] -name = "base64" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bumpalo" -version = "3.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9df67f7bf9ef8498769f994239c45613ef0c5899415fb58e9add412d2c1a538" - -[[package]] -name = "bytes" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" - -[[package]] -name = "cc" -version = "1.0.71" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd" - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "chrono" -version = "0.4.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" -dependencies = [ - "libc", - "num-integer", - "num-traits", - "serde", - "time", - "winapi", -] - -[[package]] -name = "core-foundation" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" - -[[package]] -name = "either" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" - -[[package]] -name = "encoding_rs" -version = "0.8.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - -[[package]] -name = "form_urlencoded" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" -dependencies = [ - "matches", - "percent-encoding", -] - -[[package]] -name = "futures" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d" - -[[package]] -name = "futures-executor" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377" - -[[package]] -name = "futures-macro" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18e4a4b95cea4b4ccbcf1c5675ca7c4ee4e9e75eb79944d07defde18068f79bb" -dependencies = [ - "autocfg", - "proc-macro-hack", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11" - -[[package]] -name = "futures-task" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99" - -[[package]] -name = "futures-util" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481" -dependencies = [ - "autocfg", - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "proc-macro-hack", - "proc-macro-nested", - "slab", -] - -[[package]] -name = "getrandom" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "h2" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c06815895acec637cd6ed6e9662c935b866d20a106f8361892893a7d9234964" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" - -[[package]] -name = "http" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "399c583b2979440c60be0821a6199eca73bc3c8dcd9d070d75ac726e2c6186e5" -dependencies = [ - "bytes", - "http", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503" - -[[package]] -name = "httpdate" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440" - -[[package]] -name = "hyper" -version = "0.14.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15d1cfb9e4f68655fa04c01f59edb405b6074a0f7118ea881e5026e4a1cd8593" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "hyper-tls" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" -dependencies = [ - "bytes", - "hyper", - "native-tls", - "tokio", - "tokio-native-tls", -] - -[[package]] -name = "idna" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" -dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "indexmap" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" -dependencies = [ - "autocfg", - "hashbrown", -] - -[[package]] -name = "ipnet" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9" - -[[package]] -name = "itertools" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" - -[[package]] -name = "js-sys" -version = "0.3.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "libc" -version = "0.2.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6" - -[[package]] -name = "log" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "matches" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" - -[[package]] -name = "memchr" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" - -[[package]] -name = "mime" -version = "0.3.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" - -[[package]] -name = "mio" -version = "0.7.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16" -dependencies = [ - "libc", - "log", - "miow", - "ntapi", - "winapi", -] - -[[package]] -name = "miow" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" -dependencies = [ - "winapi", -] - -[[package]] -name = "native-tls" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48ba9f7719b5a0f42f338907614285fb5fd70e53858141f69898a1fb7203b24d" -dependencies = [ - "lazy_static", - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - -[[package]] -name = "ntapi" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" -dependencies = [ - "winapi", -] - -[[package]] -name = "num-integer" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" -dependencies = [ - "autocfg", -] - -[[package]] -name = "once_cell" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" - -[[package]] -name = "openssl" -version = "0.10.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9facdb76fec0b73c406f125d44d86fdad818d66fef0531eec9233ca425ff4a" -dependencies = [ - "bitflags", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-sys", -] - -[[package]] -name = "openssl-probe" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" - -[[package]] -name = "openssl-sys" -version = "0.9.67" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69df2d8dfc6ce3aaf44b40dec6f487d5a886516cf6879c49e98e0710f310a058" -dependencies = [ - "autocfg", - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "percent-encoding" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" - -[[package]] -name = "pin-project-lite" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkg-config" -version = "0.3.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c9b1041b4387893b91ee6746cddfc28516aff326a3519fb2adf820932c5e6cb" - -[[package]] -name = "polygon" -version = "0.11.2" -source = "git+ssh://git@github.com/Overmuse/polygon?tag=v0.11.2#05dcf3b31d7ab371c0def92d35516c92d298a3c1" -dependencies = [ - "chrono", - "futures", - "itertools", - "reqwest", - "rust_decimal", - "serde", - "serde_json", - "serde_repr", - "thiserror", - "tracing", -] - -[[package]] -name = "polygon-example" -version = "0.1.0" -dependencies = [ - "backtester", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" - -[[package]] -name = "proc-macro-hack" -version = "0.5.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" - -[[package]] -name = "proc-macro-nested" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" - -[[package]] -name = "proc-macro2" -version = "1.0.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d" -dependencies = [ - "unicode-xid", -] - -[[package]] -name = "quote" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", - "rand_hc", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" -dependencies = [ - "getrandom", -] - -[[package]] -name = "rand_hc" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" -dependencies = [ - "rand_core", -] - -[[package]] -name = "redox_syscall" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" -dependencies = [ - "bitflags", -] - -[[package]] -name = "remove_dir_all" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" -dependencies = [ - "winapi", -] - -[[package]] -name = "reqwest" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51c732d463dd300362ffb44b7b125f299c23d2990411a4253824630ebc7467fb" -dependencies = [ - "base64", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "http", - "http-body", - "hyper", - "hyper-tls", - "ipnet", - "js-sys", - "lazy_static", - "log", - "mime", - "native-tls", - "percent-encoding", - "pin-project-lite", - "serde", - "serde_json", - "serde_urlencoded", - "tokio", - "tokio-native-tls", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "winreg", -] - -[[package]] -name = "rust_decimal" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0f1028de22e436bb35fce070310ee57d57b5e59ae77b4e3f24ce4773312b813" -dependencies = [ - "arrayvec", - "num-traits", - "serde", -] - -[[package]] -name = "ryu" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" - -[[package]] -name = "schannel" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" -dependencies = [ - "lazy_static", - "winapi", -] - -[[package]] -name = "security-framework" -version = "2.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525bc1abfda2e1998d152c45cf13e696f76d0a4972310b22fac1658b05df7c87" -dependencies = [ - "bitflags", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9dd14d83160b528b7bfd66439110573efcfbe281b17fc2ca9f39f550d619c7e" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "serde" -version = "1.0.130" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.130" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.68" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" -dependencies = [ - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_repr" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98d0516900518c29efa217c298fa1f4e6c6ffc85ae29fd7f4ee48f176e1a9ed5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "slab" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590" - -[[package]] -name = "socket2" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "syn" -version = "1.0.80" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - -[[package]] -name = "tempfile" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" -dependencies = [ - "cfg-if", - "libc", - "rand", - "redox_syscall", - "remove_dir_all", - "winapi", -] - -[[package]] -name = "thiserror" -version = "1.0.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "time" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" -dependencies = [ - "libc", - "wasi", - "winapi", -] - -[[package]] -name = "tinyvec" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" - -[[package]] -name = "tokio" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2c2416fdedca8443ae44b4527de1ea633af61d8f7169ffa6e72c5b53d24efcc" -dependencies = [ - "autocfg", - "bytes", - "libc", - "memchr", - "mio", - "pin-project-lite", - "winapi", -] - -[[package]] -name = "tokio-native-tls" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" -dependencies = [ - "native-tls", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d3725d3efa29485e87311c5b699de63cde14b00ed4d256b8318aa30ca452cd" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "log", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tower-service" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" - -[[package]] -name = "tracing" -version = "0.1.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105" -dependencies = [ - "cfg-if", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f480b8f81512e825f337ad51e94c1eb5d3bbdf2b363dcd01e2b19a9ffe3f8e" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing-core" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "try-lock" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" - -[[package]] -name = "unicode-bidi" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" - -[[package]] -name = "unicode-normalization" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-xid" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" - -[[package]] -name = "url" -version = "2.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" -dependencies = [ - "form_urlencoded", - "idna", - "matches", - "percent-encoding", -] - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "want" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" -dependencies = [ - "log", - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - -[[package]] -name = "wasm-bindgen" -version = "0.2.78" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.78" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" -dependencies = [ - "bumpalo", - "lazy_static", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e8d7523cb1f2a4c96c1317ca690031b714a51cc14e05f712446691f413f5d39" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.78" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.78" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.78" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" - -[[package]] -name = "web-sys" -version = "0.3.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "winreg" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" -dependencies = [ - "winapi", -] diff --git a/examples/polygon/Cargo.toml b/examples/polygon/Cargo.toml deleted file mode 100644 index d2c154e..0000000 --- a/examples/polygon/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "polygon" -version = "0.1.0" -edition = "2018" -publish = false - -[dependencies] -anyhow = "1.0.44" -backtester = { path = "../..", features = ["polygon"] } -chrono = "0.4" -dotenv = "0.15.0" -rust_decimal = "1.16.0" -tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } diff --git a/examples/polygon/src/main.rs b/examples/polygon/src/main.rs deleted file mode 100644 index 38fe679..0000000 --- a/examples/polygon/src/main.rs +++ /dev/null @@ -1,44 +0,0 @@ -use anyhow::Result; -use backtester::data::downloader::polygon::PolygonDownloader; -use backtester::finance::commission::PerDollarCommission; -use backtester::prelude::*; -use chrono::NaiveDate; -use dotenv::dotenv; -use rust_decimal::Decimal; - -struct Strat; - -impl Strategy for Strat { - type Error = anyhow::Error; - - fn at_open(&mut self, brokerage: &mut Brokerage, market: &Market) -> Result<(), Self::Error> { - let e = market.get_last_price("E"); - let m = market.get_last_price("M"); - if let (Some(e), Some(m)) = (e, m) { - let order = if e > m { - Order::new("E", Decimal::ONE).limit_price(e) - } else { - Order::new("M", Decimal::ONE).limit_price(m) - }; - brokerage.send_order(order); - } - Ok(()) - } -} - -#[tokio::main] -async fn main() -> Result<()> { - let _ = dotenv(); - let downloader = PolygonDownloader.file_cache("data"); - let meta = DataOptions::new( - vec!["E".to_string(), "M".to_string()], - NaiveDate::from_ymd(2011, 1, 1), - NaiveDate::from_ymd(2020, 12, 31), - ); - let data = downloader.download_data(&meta).await?; - let market = Market::new(data); - let brokerage = Brokerage::new(Decimal::new(100000, 0), market) - .commission(PerDollarCommission::new(Decimal::new(1, 3))); - let mut simulator = Simulator::new(brokerage, Strat); - simulator.run() -} diff --git a/examples/random_trades.rs b/examples/random_trades.rs new file mode 100644 index 0000000..c26fcec --- /dev/null +++ b/examples/random_trades.rs @@ -0,0 +1,57 @@ +use anyhow::Result; +use async_trait::async_trait; +use backtester::prelude::*; +use chrono::NaiveDate; +use dotenv::dotenv; +use rand::prelude::*; +use rust_decimal::Decimal; +use tracing_subscriber::EnvFilter; + +struct Strat; + +#[async_trait] +impl Strategy for Strat { + type Error = anyhow::Error; + + #[tracing::instrument(skip(self, brokerage, market))] + async fn at_open(&mut self, brokerage: Brokerage, market: Market) -> Result<(), Self::Error> { + let e = market.get_last_price("E").await; + let m = market.get_last_price("M").await; + tracing::info!(?e, ?m, "prices"); + let equity = Decimal::new(10000, 0); + if let (Some(e), Some(m)) = (e, m) { + let amount = if random::() { equity } else { -equity }; + + let order = if e > m { + Order::new("E", amount).limit_price(e) + } else { + Order::new("M", amount).limit_price(m) + }; + brokerage.send_order(order).await; + } + Ok(()) + } + + #[tracing::instrument(skip(self, brokerage, _market))] + async fn at_close(&mut self, brokerage: Brokerage, _market: Market) -> Result<(), Self::Error> { + brokerage.close_positions().await; + Ok(()) + } +} + +#[tokio::main] +async fn main() -> Result<()> { + let _ = dotenv(); + let subscriber = tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .finish(); + tracing::subscriber::set_global_default(subscriber)?; + let data_options = Options::new( + vec!["E".to_string(), "M".to_string()], + NaiveDate::from_ymd(2020, 1, 1), + NaiveDate::from_ymd(2020, 12, 31), + ) + .set_resolution(Resolution::Minute); + let simulator = Simulator::new(Decimal::new(100000, 0), Strat, data_options); + simulator.run().await +} diff --git a/src/brokerage/account.rs b/src/brokerage/account.rs index dde3235..88e0f6d 100644 --- a/src/brokerage/account.rs +++ b/src/brokerage/account.rs @@ -8,7 +8,6 @@ pub struct Account { pub inactive_orders: Vec, pub positions: HashMap, pub cash: Decimal, - starting_cash: Decimal, } impl Account { @@ -18,7 +17,6 @@ impl Account { inactive_orders: Vec::new(), positions: HashMap::new(), cash, - starting_cash: cash, } } @@ -37,19 +35,13 @@ impl Account { .unwrap_or(Decimal::ZERO) * price } - - pub fn reset(&mut self) { - self.cash = self.starting_cash; - self.active_orders.clear(); - self.inactive_orders.clear(); - self.positions.clear() - } } #[cfg(test)] mod test { use super::*; - use chrono::Utc; + use chrono::TimeZone; + use chrono_tz::US::Eastern; #[test] fn it_can_be_initialized() { @@ -58,15 +50,6 @@ mod test { assert!(account.inactive_orders.is_empty()); assert!(account.positions.is_empty()); assert_eq!(account.cash, Decimal::ONE_HUNDRED); - assert_eq!(account.starting_cash, Decimal::ONE_HUNDRED); - } - - #[test] - fn it_can_be_reset() { - let mut account = Account::new(Decimal::ONE_HUNDRED); - account.cash = Decimal::new(200, 0); - account.reset(); - assert_eq!(account.cash, Decimal::ONE_HUNDRED); } #[test] @@ -75,7 +58,7 @@ mod test { account.add_lot( "AAPL".into(), Lot { - fill_time: Utc::now(), + fill_time: Eastern.ymd(2021, 1, 1).and_hms(0, 0, 0), price: Decimal::new(2, 0), quantity: Decimal::new(3, 0), }, diff --git a/src/brokerage/actor.rs b/src/brokerage/actor.rs new file mode 100644 index 0000000..9101e65 --- /dev/null +++ b/src/brokerage/actor.rs @@ -0,0 +1,300 @@ +use crate::brokerage::account::Account; +use crate::brokerage::handle::*; +use crate::brokerage::order::{Order, OrderStatus}; +use crate::brokerage::position::Lot; +use crate::finance::{ + commission::{Commission, NoCommission}, + slippage::{NoSlippage, Slippage}, +}; +use crate::markets::handle::Market; +use chrono::DateTime; +use chrono_tz::Tz; +use futures::StreamExt; +use rust_decimal::Decimal; +use serde::Serialize; +use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; +use tokio::sync::oneshot::Sender as OneshotSender; +use tracing::{debug, trace}; + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum Event { + Commission { + amount: Decimal, + }, + OrderUpdate { + status: OrderStatus, + time: DateTime, + order: Order, + }, +} + +pub struct BrokerageActor { + requests: UnboundedReceiver<(OneshotSender, BrokerageRequest)>, + account: Account, + market: Market, + commission: Box, + _slippage: Box, + listeners: Vec>, +} + +impl BrokerageActor { + pub fn spawn(cash: Decimal, market: Market) -> Brokerage { + let account = Account::new(cash); + + let (tx, rx) = unbounded_channel(); + let handle = Brokerage::new(tx); + + let actor = Self { + requests: rx, + account, + market, + commission: Box::new(NoCommission), + _slippage: Box::new(NoSlippage), + listeners: Vec::new(), + }; + tokio::spawn(async move { actor.run_forever().await }); + handle + } + + async fn run_forever(mut self) { + while let Some((tx, request)) = self.requests.recv().await { + let response = self.handle_message(request).await; + tx.send(response).unwrap() + } + } + + #[tracing::instrument(skip(self, request))] + async fn handle_message(&mut self, request: BrokerageRequest) -> BrokerageResponse { + match request { + BrokerageRequest::GetPositions => { + BrokerageResponse::Positions(self.account.positions.values().cloned().collect()) + } + BrokerageRequest::CancelActiveOrders => { + self.cancel_active_orders().await; + BrokerageResponse::Success + } + BrokerageRequest::GetEquity => BrokerageResponse::Decimal(self.get_equity().await), + BrokerageRequest::ClosePositions => { + self.close_positions().await; + BrokerageResponse::Success + } + BrokerageRequest::SendOrder(order) => { + self.send_order(order).await; + BrokerageResponse::Success + } + BrokerageRequest::ReconcileOrders => { + self.reconcile_active_orders().await; + BrokerageResponse::Success + } + BrokerageRequest::ExpireOrders => { + self.expire_orders().await; + BrokerageResponse::Success + } + BrokerageRequest::Subscribe => { + let receiver = self.subscribe(); + BrokerageResponse::EventListener(receiver) + } + } + } + + #[tracing::instrument(skip(self))] + async fn get_equity(&self) -> Decimal { + let tickers = self.account.positions.keys(); + let equity = futures::stream::iter(tickers) + .fold(self.account.cash, |equity, ticker| async move { + let price = self.market.get_current_price(ticker).await; + let position_value = self + .account + .market_value(ticker, price.unwrap_or(Decimal::ZERO)); + equity + position_value + }) + .await; + equity + } + + #[tracing::instrument(skip(self))] + async fn close_positions(&mut self) { + let orders: Vec = self + .account + .positions + .values() + .filter_map(|pos| { + let qty = pos.quantity(); + if qty.is_zero() { + None + } else { + Some(Order::new(pos.ticker.clone(), -qty)) + } + }) + .collect(); + for order in orders { + debug!(id = %order.id, "Closing order"); + self.send_order(order).await + } + } + + #[tracing::instrument(skip(self, order), fields(id = %order.id))] + async fn send_order(&mut self, order: Order) { + if self.market.is_open().await { + let market = self.market.clone(); + let (_, current_price) = futures::join!( + self.save_order(&order), + market.get_current_price(&order.ticker) + ); + if let Some(price) = current_price { + if order.is_marketable(price) { + self.fill_order(order, price).await; + } + } + } else { + trace!("Market closed"); + self.reject_order(order).await; + } + } + + #[tracing::instrument(skip(self, order, price))] + async fn fill_order(&mut self, order: Order, price: Decimal) { + let fill_time = self.market.datetime().await; + debug!(%fill_time, %price, "Order filled"); + let lot = Lot { + fill_time, + price, + quantity: order.shares, + }; + let commission = self.commission.calculate(&lot); + self.account.add_lot(order.ticker.clone(), lot); + self.account.cash -= commission; + self.account.inactive_orders.push(order.clone()); + self.account.active_orders.retain(|o| o.id != order.id); + let event = Event::OrderUpdate { + status: OrderStatus::Filled { + fill_time, + average_fill_price: price, + }, + time: fill_time, + order, + }; + self.report_event(&event); + if !commission.is_zero() { + let event = Event::Commission { amount: commission }; + self.report_event(&event); + } + } + + #[tracing::instrument(skip(self, order))] + async fn save_order(&mut self, order: &Order) { + debug!("Order saved"); + self.account.active_orders.push(order.clone()); + let event = Event::OrderUpdate { + status: OrderStatus::Submitted, + time: self.market.datetime().await, + order: order.clone(), + }; + self.report_event(&event) + } + + #[tracing::instrument(skip(self, order))] + async fn reject_order(&mut self, order: Order) { + debug!("Order rejected"); + self.account.inactive_orders.push(order.clone()); + let event = Event::OrderUpdate { + status: OrderStatus::Rejected, + time: self.market.datetime().await, + order, + }; + self.report_event(&event) + } + + #[tracing::instrument(skip(self, order), fields(id = %order.id))] + async fn expire_order(&mut self, order: Order) { + debug!("Order expired"); + self.account.inactive_orders.push(order.clone()); + let event = Event::OrderUpdate { + status: OrderStatus::Expired, + time: self.market.datetime().await, + order, + }; + self.report_event(&event) + } + + #[tracing::instrument(skip(self, order), fields(id = %order.id))] + async fn cancel_order(&mut self, order: Order) { + debug!("Order cancelled"); + self.account.inactive_orders.push(order.clone()); + let event = Event::OrderUpdate { + status: OrderStatus::Cancelled, + time: self.market.datetime().await, + order, + }; + self.report_event(&event) + } + + fn report_event(&self, event: &Event) { + for listener in self.listeners.iter() { + listener + .send(event.clone()) + .expect("Failed to report event"); + } + } + + #[tracing::instrument(skip(self))] + async fn reconcile_active_orders(&mut self) { + // Manual version of drain_filter to be able to use the stable toolchain + // TODO: Change to use drain_filter once https://github.com/rust-lang/rust/issues/43244 is + // merged. + let mut i = 0; + let v = &mut self.account.active_orders; + let mut orders_to_send: Vec = Vec::new(); + while i < v.len() { + let order = &v[i]; + let price = self.market.get_current_price(&order.ticker).await; + if let Some(price) = price { + if order.is_marketable(price) { + let val = v.remove(i); + orders_to_send.push(val); + } else { + i += 1 + } + } else { + i += 1 + } + } + for order in orders_to_send { + let price = self + .market + .get_current_price(&order.ticker) + .await + .expect("Guaranteed to exist"); + self.fill_order(order, price).await + } + } + + #[tracing::instrument(skip(self))] + async fn cancel_active_orders(&mut self) { + loop { + let maybe_order = self.account.active_orders.pop(); + match maybe_order { + Some(order) => self.cancel_order(order).await, + None => return, + } + } + } + + #[tracing::instrument(skip(self))] + async fn expire_orders(&mut self) { + loop { + let maybe_order = self.account.active_orders.pop(); + match maybe_order { + Some(order) => self.expire_order(order).await, + None => return, + } + } + } + + fn subscribe(&mut self) -> UnboundedReceiver { + let (tx, rx) = unbounded_channel(); + self.listeners.push(tx); + rx + } +} diff --git a/src/brokerage/handle.rs b/src/brokerage/handle.rs new file mode 100644 index 0000000..7b622cc --- /dev/null +++ b/src/brokerage/handle.rs @@ -0,0 +1,103 @@ +use crate::brokerage::actor::Event; +use crate::brokerage::order::Order; +use crate::brokerage::position::Position; +use rust_decimal::Decimal; +use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; +use tokio::sync::oneshot::{self, Sender as OneshotSender}; + +#[derive(Clone, Debug)] +pub(crate) enum BrokerageRequest { + GetPositions, + GetEquity, + CancelActiveOrders, + ClosePositions, + SendOrder(Order), + ReconcileOrders, + ExpireOrders, + Subscribe, +} + +#[derive(Debug)] +pub(crate) enum BrokerageResponse { + Positions(Vec), + Decimal(Decimal), + EventListener(UnboundedReceiver), + // Generic reply for when no reply is needed + Success, +} + +#[derive(Clone)] +pub struct Brokerage { + sender: UnboundedSender<(OneshotSender, BrokerageRequest)>, +} + +impl Brokerage { + pub(crate) fn new( + sender: UnboundedSender<(OneshotSender, BrokerageRequest)>, + ) -> Self { + Self { sender } + } + + #[tracing::instrument(skip(self, request))] + async fn send_request(&self, request: BrokerageRequest) -> BrokerageResponse { + let (tx, rx) = oneshot::channel(); + self.sender.send((tx, request)).unwrap(); + rx.await.unwrap() + } + + #[tracing::instrument(skip(self))] + pub async fn get_positions(&self) -> Vec { + let response = self.send_request(BrokerageRequest::GetPositions).await; + if let BrokerageResponse::Positions(positions) = response { + positions + } else { + unreachable!() + } + } + + #[tracing::instrument(skip(self))] + pub async fn get_equity(&self) -> Decimal { + let response = self.send_request(BrokerageRequest::GetEquity).await; + if let BrokerageResponse::Decimal(equity) = response { + equity + } else { + unreachable!() + } + } + + #[tracing::instrument(skip(self))] + pub async fn close_positions(&self) { + self.send_request(BrokerageRequest::ClosePositions).await; + } + + #[tracing::instrument(skip(self))] + pub async fn send_order(&self, order: Order) { + self.send_request(BrokerageRequest::SendOrder(order)).await; + } + + #[tracing::instrument(skip(self))] + pub async fn subscribe(&self) -> UnboundedReceiver { + let response = self.send_request(BrokerageRequest::Subscribe).await; + if let BrokerageResponse::EventListener(receiver) = response { + receiver + } else { + unreachable!() + } + } + + #[tracing::instrument(skip(self))] + pub async fn cancel_active_orders(&self) { + self.send_request(BrokerageRequest::CancelActiveOrders) + .await; + } + + #[tracing::instrument(skip(self))] + pub(crate) async fn reconcile_active_orders(&self) { + self.send_request(BrokerageRequest::ReconcileOrders).await; + } + + #[tracing::instrument(skip(self))] + pub(crate) async fn expire_orders(&self) { + self.send_request(BrokerageRequest::ExpireOrders).await; + } +} diff --git a/src/brokerage/mod.rs b/src/brokerage/mod.rs index bef6882..b92377f 100644 --- a/src/brokerage/mod.rs +++ b/src/brokerage/mod.rs @@ -1,207 +1,5 @@ -use crate::finance::{ - commission::{Commission, NoCommission}, - slippage::{NoSlippage, Slippage}, -}; -use crate::markets::market::Market; -use account::Account; -use chrono::{DateTime, Utc}; -use order::Order; -use position::{Lot, Position}; -use rust_decimal::Decimal; -use std::sync::mpsc::{channel, Receiver, Sender}; - pub mod account; +pub mod actor; +pub mod handle; pub mod order; pub mod position; - -#[derive(Clone, Debug)] -pub enum Event { - Commission { - amount: Decimal, - }, - OrderUpdate { - status: OrderStatus, - time: DateTime, - order: Order, - }, -} - -pub struct Brokerage { - account: Account, - market: Market, - commission: Box, - slippage: Box, - listeners: Vec>, -} - -#[derive(Clone, Debug)] -pub enum OrderStatus { - Submitted, - Cancelled, - Filled { - fill_time: DateTime, - average_fill_price: Decimal, - }, - PartiallyFilled, - Rejected, -} - -#[derive(Debug)] -pub struct BrokerageOrder { - status: OrderStatus, - order: Order, -} - -impl Brokerage { - pub fn new(cash: Decimal, market: Market) -> Self { - let account = Account::new(cash); - Self { - account, - market, - commission: Box::new(NoCommission), - slippage: Box::new(NoSlippage), - listeners: Vec::new(), - } - } - - pub fn get_account(&self) -> &Account { - &self.account - } - - pub fn get_positions(&self) -> Vec<&Position> { - self.account.positions.values().collect() - } - - pub fn get_equity(&self) -> Decimal { - let tickers = self.account.positions.keys(); - let positions_value: Decimal = tickers - .map(|ticker| (ticker, self.market.get_current_price(ticker))) - .map(|(ticker, price)| { - self.account - .market_value(ticker, price.unwrap_or(Decimal::ZERO)) - }) - .sum(); - positions_value + self.account.cash - } - - pub fn commission(mut self, commission: C) -> Self { - self.commission = Box::new(commission); - self - } - - pub fn slippage(mut self, slippage: S) -> Self { - self.slippage = Box::new(slippage); - self - } - - fn fill_order(&mut self, order: Order, price: Decimal) { - let fill_time = self.market.datetime(); - let lot = Lot { - fill_time, - price, - quantity: order.shares, - }; - let commission = self.commission.calculate(&lot); - self.account.add_lot(order.ticker.clone(), lot); - self.account.cash -= commission; - self.account.inactive_orders.push(order.clone()); - let time = self.market.datetime(); - let event = Event::OrderUpdate { - status: OrderStatus::Filled { - fill_time: time, - average_fill_price: price, - }, - time, - order, - }; - self.report_event(&event); - let event = Event::Commission { amount: commission }; - self.report_event(&event); - } - - fn save_order(&mut self, order: &Order) { - self.account.active_orders.push(order.clone()); - let event = Event::OrderUpdate { - status: OrderStatus::Submitted, - time: self.market.datetime(), - order: order.clone(), - }; - self.report_event(&event) - } - - fn reject_order(&mut self, order: Order) { - self.account.inactive_orders.push(order.clone()); - let event = Event::OrderUpdate { - status: OrderStatus::Rejected, - time: self.market.datetime(), - order, - }; - self.report_event(&event) - } - - pub fn send_order(&mut self, order: Order) { - if self.market.is_open() { - self.save_order(&order); - let current_price = self.market.get_current_price(&order.ticker); - if let Some(price) = current_price { - if order.is_marketable(price) { - self.fill_order(order, price); - } - } - } else { - self.reject_order(order); - } - } - - fn report_event(&self, event: &Event) { - for listener in self.listeners.iter() { - listener - .send(event.clone()) - .expect("Failed to report event"); - } - } - - pub(crate) fn reconcile_active_orders(&mut self) { - // TODO: This whole function is very inefficient - - // Can clone cheaply here due to RC - let market = self.market.clone(); - - // Manual version of drain_filter to be able to use the stable toolchain - // TODO: Change to use drain_filter once https://github.com/rust-lang/rust/issues/43244 is - // merged. - let mut i = 0; - let v = &mut self.account.active_orders; - let mut orders_to_send: Vec = Vec::new(); - while i < v.len() { - let order = &v[i]; - let price = market.get_current_price(&order.ticker); - if let Some(price) = price { - if order.is_marketable(price) { - let val = v.remove(i); - orders_to_send.push(val); - } else { - i += 1 - } - } else { - i += 1 - } - } - for order in orders_to_send { - let price = market - .get_current_price(&order.ticker) - .expect("Guaranteed to exist"); - self.fill_order(order, price) - } - } - - pub fn subscribe(&mut self) -> Receiver { - let (tx, rx) = channel(); - self.listeners.push(tx); - rx - } - - pub(crate) fn get_market(&self) -> Market { - self.market.clone() - } -} diff --git a/src/brokerage/order.rs b/src/brokerage/order.rs index d83237e..0841323 100644 --- a/src/brokerage/order.rs +++ b/src/brokerage/order.rs @@ -1,7 +1,25 @@ +use chrono::DateTime; +use chrono_tz::Tz; use rust_decimal::Decimal; +use serde::Serialize; use uuid::Uuid; -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum OrderStatus { + Submitted, + Cancelled, + Filled { + fill_time: DateTime, + average_fill_price: Decimal, + }, + PartiallyFilled, + Rejected, + Expired, +} + +#[derive(Debug, Clone, Copy, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] pub enum OrderType { Market, Limit(Decimal), @@ -9,7 +27,7 @@ pub enum OrderType { StopLimit(Decimal, Decimal), } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize)] pub struct Order { pub id: Uuid, pub ticker: String, @@ -22,7 +40,7 @@ impl Order { Self { id: Uuid::new_v4(), ticker: ticker.to_string(), - shares, + shares: shares.round_dp(8), order_type: OrderType::Market, } } diff --git a/src/brokerage/position.rs b/src/brokerage/position.rs index 24b722f..658a08b 100644 --- a/src/brokerage/position.rs +++ b/src/brokerage/position.rs @@ -1,4 +1,5 @@ -use chrono::{DateTime, Utc}; +use chrono::DateTime; +use chrono_tz::Tz; use num_traits::Signed; use rust_decimal::prelude::*; use std::collections::VecDeque; @@ -6,7 +7,7 @@ use std::fmt; #[derive(Clone, Debug)] pub struct Lot { - pub fill_time: DateTime, + pub fill_time: DateTime, pub price: Decimal, pub quantity: Decimal, } @@ -102,6 +103,8 @@ impl Position { #[cfg(test)] mod test { use super::*; + use chrono::Utc; + use chrono_tz::US::Eastern; #[test] fn it_can_do_fifo_lot_aggregation() { @@ -109,7 +112,7 @@ mod test { let mut position = Position::new( "AAPL".to_string(), Lot { - fill_time: Utc::now(), + fill_time: Utc::now().with_timezone(&Eastern), price, quantity: Decimal::new(2, 0), }, @@ -122,7 +125,7 @@ mod test { price = Decimal::new(150, 0); position.add_lot(Lot { - fill_time: Utc::now(), + fill_time: Utc::now().with_timezone(&Eastern), price, quantity: Decimal::new(3, 0), }); @@ -134,7 +137,7 @@ mod test { price = Decimal::new(120, 0); position.add_lot(Lot { - fill_time: Utc::now(), + fill_time: Utc::now().with_timezone(&Eastern), price, quantity: Decimal::new(-1, 0), }); @@ -145,7 +148,7 @@ mod test { assert_eq!(position.unrealized_profit(price), Decimal::new(-70, 0)); position.add_lot(Lot { - fill_time: Utc::now(), + fill_time: Utc::now().with_timezone(&Eastern), price, quantity: Decimal::new(-3, 0), }); @@ -156,7 +159,7 @@ mod test { assert_eq!(position.unrealized_profit(price), Decimal::new(-30, 0)); position.add_lot(Lot { - fill_time: Utc::now(), + fill_time: Utc::now().with_timezone(&Eastern), price, quantity: Decimal::new(-3, 0), }); @@ -168,7 +171,7 @@ mod test { price = Decimal::new(80, 0); position.add_lot(Lot { - fill_time: Utc::now(), + fill_time: Utc::now().with_timezone(&Eastern), price, quantity: Decimal::new(2, 0), }); diff --git a/src/data/cache.rs b/src/data/cache.rs index b55f239..0bbf623 100644 --- a/src/data/cache.rs +++ b/src/data/cache.rs @@ -1,4 +1,4 @@ -use super::{error::Error, DataOptions, DataProvider, MarketData}; +use super::{error::Error, provider::DataProvider, DataOptions, MarketData}; use async_trait::async_trait; use std::fs::OpenOptions; use std::io::prelude::*; @@ -10,7 +10,7 @@ pub trait DataCache { fn data_provider(&self) -> &Self::DataProvider; fn is_cache_valid(&self, meta: &DataOptions) -> bool; - fn save_data(&self, data: &MarketData) -> Result<(), Error>; + fn save_data(&self, meta: &DataOptions, data: &MarketData) -> Result<(), Error>; async fn load_data(&self, meta: &DataOptions) -> Result; } @@ -33,9 +33,16 @@ impl FileCache for T { impl DataProvider for T where T: DataCache + Sync + Send, + T::DataProvider: DataProvider + Send + Sync, { async fn download_data(&self, meta: &DataOptions) -> Result { - self.load_data(meta).await + if self.is_cache_valid(meta) { + self.load_data(meta).await + } else { + let data = self.data_provider().download_data(meta).await?; + self.save_data(meta, &data)?; + Ok(data) + } } } @@ -65,11 +72,11 @@ impl DataCache for FileDataCache { fn is_cache_valid(&self, meta: &DataOptions) -> bool { let mut path = self.dir.clone(); - path.push("meta.json"); + path.push("meta.data"); if path.exists() { let bytes = std::fs::read(path); if let Ok(bytes) = bytes { - let cached_meta = serde_json::from_slice::(&bytes); + let cached_meta = rmp_serde::from_slice::(&bytes); if let Ok(cached_meta) = cached_meta { let ticker_check = meta .tickers @@ -85,38 +92,51 @@ impl DataCache for FileDataCache { false } - fn save_data(&self, data: &MarketData) -> Result<(), Error> { + fn save_data(&self, meta: &DataOptions, data: &MarketData) -> Result<(), Error> { + let mut path = self.dir.clone(); + std::fs::create_dir_all(path.clone())?; + path.push("meta.data"); + let mut file = OpenOptions::new().create(true).write(true).open(path)?; + let bytes = rmp_serde::to_vec(meta)?; + file.write_all(&bytes)?; let mut path = self.dir.clone(); - path.push("data.json"); + path.push("prices.data"); let mut file = OpenOptions::new().create(true).write(true).open(path)?; - let bytes = serde_json::to_vec_pretty(&data)?; + let bytes = rmp_serde::to_vec(&data)?; file.write_all(&bytes)?; Ok(()) } async fn load_data(&self, meta: &DataOptions) -> Result { let mut path = self.dir.clone(); - if self.is_cache_valid(meta) { - path.push("data.json"); - let bytes = std::fs::read(path)?; - let mut data: MarketData = serde_json::from_slice(&bytes)?; - data.prices - .retain(|ticker, _| meta.tickers.contains(ticker)); - data.prices.values_mut().for_each(|timeseries| { - timeseries.retain(|dt, _| { - dt.naive_utc().date() > meta.start && dt.naive_utc().date() <= meta.end - }) - }); - Ok(data) - } else { - std::fs::create_dir_all(path.clone())?; - path.push("meta.json"); - let mut file = OpenOptions::new().create(true).write(true).open(path)?; - let bytes = serde_json::to_vec(meta)?; - file.write_all(&bytes)?; - let data = self.data_provider().download_data(meta).await?; - self.save_data(&data)?; - Ok(data) - } + path.push("prices.data"); + let bytes = std::fs::read(path)?; + let mut data: MarketData = rmp_serde::from_slice(&bytes)?; + let t_idx = data + .timestamps + .iter() + .map(|t| t.naive_utc().date() >= meta.start && t.naive_utc().date() <= meta.end); + let (new_data, new_timestamps) = data + .data + .iter() + .zip(data.timestamps.clone()) + .zip(t_idx) + .filter_map(|((r, t), i)| if i { Some((r.clone(), t)) } else { None }) + .unzip(); + // TODO: Filter tickers + //let ticker_idx = data + // .tickers + // .iter() + // .map(|ticker| meta.tickers.contains(ticker)); + //let (new_data, new_tickers) = new_data + // .data + // .iter() + // .zip(data.timestamps.clone()) + // .zip(t_idx) + // .filter_map(|((r, t), i)| if i { Some((r.clone(), t)) } else { None }) + // .unzip(); + data.data = new_data; + data.timestamps = new_timestamps; + Ok(data) } } diff --git a/src/data/downloader/mod.rs b/src/data/downloader/mod.rs deleted file mode 100644 index 6be15ce..0000000 --- a/src/data/downloader/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -#[cfg(feature = "polygon")] -pub mod polygon; diff --git a/src/data/downloader/polygon.rs b/src/data/downloader/polygon.rs deleted file mode 100644 index 7d91816..0000000 --- a/src/data/downloader/polygon.rs +++ /dev/null @@ -1,48 +0,0 @@ -use crate::data::{error::Error, Aggregate, DataOptions, DataProvider, MarketData, PriceData}; -use ::polygon::rest::{Aggregate as PolygonAggregate, AggregateWrapper, Client, GetAggregate}; -use async_trait::async_trait; - -pub struct PolygonDownloader; - -impl From for Aggregate { - fn from(p: PolygonAggregate) -> Aggregate { - Aggregate { - datetime: p.t, - open: p.o, - high: p.h, - low: p.l, - close: p.c, - volume: p.v, - } - } -} - -#[async_trait] -impl DataProvider for PolygonDownloader { - async fn download_data(&self, meta: &DataOptions) -> Result { - let client = Client::from_env()?; - let queries = meta - .tickers - .iter() - .map(|ticker| GetAggregate::new(ticker, meta.start, meta.end)); - let wrappers: Result, Error> = client - .send_all(queries) - .await - .into_iter() - .map(|x| x.map_err(From::from)) - .collect(); - let prices: PriceData = wrappers? - .into_iter() - .map(|w| (w.ticker, w.results.unwrap_or_default())) - .map(|(ticker, data)| { - ( - ticker, - data.into_iter() - .map(|agg| (agg.t, From::from(agg))) - .collect(), - ) - }) - .collect(); - Ok(MarketData { prices }) - } -} diff --git a/src/data/error.rs b/src/data/error.rs index 14ce6cd..9578758 100644 --- a/src/data/error.rs +++ b/src/data/error.rs @@ -4,8 +4,6 @@ use thiserror::Error; pub enum Error { #[error("{0}")] Io(std::io::Error), - #[error("{0}")] - Serde(serde_json::Error), #[cfg(feature = "polygon")] #[error("{0}")] Polygon(::polygon::errors::Error), @@ -16,11 +14,7 @@ impl From for Error { Self::Io(e) } } -impl From for Error { - fn from(e: serde_json::Error) -> Self { - Self::Serde(e) - } -} + #[cfg(feature = "polygon")] impl From<::polygon::errors::Error> for Error { fn from(e: ::polygon::errors::Error) -> Self { diff --git a/src/data/mod.rs b/src/data/mod.rs index 8f033af..066a2b2 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -1,34 +1,16 @@ -use async_trait::async_trait; -use chrono::{DateTime, NaiveDate, Utc}; +use crate::utils::serde_tz; +use chrono::{DateTime, NaiveTime, TimeZone}; +use chrono_tz::{Tz, US::Eastern}; +use polygon::rest::Aggregate as PolygonAggregate; use rust_decimal::prelude::*; use serde::{Deserialize, Serialize}; -use std::collections::{BTreeMap, HashMap}; -pub use cache::FileCache; -mod cache; -pub mod downloader; pub mod error; -#[derive(Deserialize, Serialize, Debug, Clone)] -pub struct DataOptions { - tickers: Vec, - start: NaiveDate, - end: NaiveDate, -} - -impl DataOptions { - pub fn new(tickers: Vec, start: NaiveDate, end: NaiveDate) -> Self { - Self { - tickers, - start, - end, - } - } -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Aggregate { - pub datetime: DateTime, + #[serde(with = "serde_tz")] + pub datetime: DateTime, pub open: Decimal, pub high: Decimal, pub low: Decimal, @@ -36,14 +18,35 @@ pub struct Aggregate { pub volume: Decimal, } -type PriceData = HashMap, Aggregate>>; +impl From for Aggregate { + fn from(p: PolygonAggregate) -> Aggregate { + Aggregate { + datetime: p.t.with_timezone(&Eastern), + open: p.o, + high: p.h, + low: p.l, + close: p.c, + volume: p.v, + } + } +} -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct MarketData { - pub prices: PriceData, +pub trait MarketTimeExt { + fn is_regular_hours(&self) -> bool; + fn is_opening(&self) -> bool; + fn is_closing(&self) -> bool; } -#[async_trait] -pub trait DataProvider { - async fn download_data(&self, meta: &DataOptions) -> Result; +impl MarketTimeExt for DateTime { + fn is_regular_hours(&self) -> bool { + let zoned = self.with_timezone(&Eastern); + (zoned.time() >= NaiveTime::from_hms(9, 30, 00)) + && (zoned.time() < NaiveTime::from_hms(16, 00, 00)) + } + fn is_opening(&self) -> bool { + self.with_timezone(&Eastern).time() == NaiveTime::from_hms(9, 30, 0) + } + fn is_closing(&self) -> bool { + self.with_timezone(&Eastern).time() == NaiveTime::from_hms(16, 0, 0) + } } diff --git a/src/finance/commission.rs b/src/finance/commission.rs index 887077c..a6acb84 100644 --- a/src/finance/commission.rs +++ b/src/finance/commission.rs @@ -1,7 +1,7 @@ use crate::brokerage::position::Lot; use rust_decimal::prelude::*; -pub trait Commission { +pub trait Commission: Send + Sync { fn calculate(&self, lot: &Lot) -> Decimal; } @@ -82,6 +82,7 @@ impl Commission for PerDollarCommission { mod test { use super::*; use chrono::Utc; + use chrono_tz::US::Eastern; #[test] fn it_calculates_the_correct_commission_amount() { @@ -91,7 +92,7 @@ mod test { let per_dollar_commission = PerDollarCommission::new(Decimal::new(3, 0)); let lot = Lot { - fill_time: Utc::now(), + fill_time: Utc::now().with_timezone(&Eastern), quantity: Decimal::new(4, 0), price: Decimal::new(5, 0), }; @@ -110,7 +111,7 @@ mod test { PerDollarCommission::new(Decimal::new(3, 0)).min_lot_cost(Decimal::new(100, 0)); let lot = Lot { - fill_time: Utc::now(), + fill_time: Utc::now().with_timezone(&Eastern), quantity: Decimal::new(4, 0), price: Decimal::new(5, 0), }; diff --git a/src/finance/slippage.rs b/src/finance/slippage.rs index 2481d8b..a7bd9c4 100644 --- a/src/finance/slippage.rs +++ b/src/finance/slippage.rs @@ -1,4 +1,4 @@ -pub trait Slippage { +pub trait Slippage: Send + Sync { fn slippage(&self, volume_share: f64) -> f64; } diff --git a/src/lib.rs b/src/lib.rs index 20e55db..4a4fdda 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,24 +1,33 @@ +#[macro_use] +extern crate lazy_static; + mod brokerage; pub mod data; pub mod finance; mod markets; +mod options; mod simulator; pub mod statistics; mod strategy; +mod utils; pub use brokerage::{ - order::{Order, OrderType}, - Brokerage, OrderStatus, + actor::Event, + handle::Brokerage, + order::{Order, OrderStatus, OrderType}, }; -pub use markets::{clock::MarketState, market::Market}; +pub use data::Aggregate; +pub use markets::{clock::MarketState, handle::Market}; +pub use options::{Options, Resolution}; pub use simulator::Simulator; pub use strategy::Strategy; pub mod prelude { - pub use crate::data::{DataOptions, DataProvider, FileCache}; + pub use crate::data::MarketTimeExt; pub use crate::{ - brokerage::{order::Order, Brokerage}, - markets::market::Market, + brokerage::{handle::Brokerage, order::Order}, + markets::handle::Market, + options::{Options, Resolution}, simulator::Simulator, strategy::Strategy, }; diff --git a/src/markets/actor.rs b/src/markets/actor.rs new file mode 100644 index 0000000..9bbe59c --- /dev/null +++ b/src/markets/actor.rs @@ -0,0 +1,158 @@ +use crate::markets::clock::{Clock, MarketState}; +use crate::markets::data_manager::DataManager; +use crate::markets::handle::*; +use crate::utils::progress::progress; +use crate::Aggregate; +use crate::Options; +use chrono::prelude::*; +use chrono_tz::Tz; +use indicatif::ProgressBar; +use rust_decimal::Decimal; +use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver}; +use tokio::sync::oneshot::Sender as OneshotSender; +use tracing::{debug, trace}; + +pub(crate) struct MarketActor { + requests: UnboundedReceiver<(OneshotSender, MarketRequest)>, + data_manager: DataManager, + clock: Clock, + progress: ProgressBar, +} + +impl MarketActor { + pub fn spawn(data_options: Options) -> Market { + let clock = Clock::new( + data_options.start, + data_options.end, + data_options.warmup, + data_options.resolution, + ); + let progress = progress(clock.simulation_periods() as u64, "Simulating"); + let data_manager = DataManager::new(data_options); + let (tx, rx) = unbounded_channel(); + let handle = Market::new(tx); + + let actor = Self { + requests: rx, + data_manager, + clock, + progress, + }; + tokio::spawn(async move { actor.run_forever().await }); + handle + } + + async fn run_forever(mut self) { + self.data_manager.download_data().await; + self.progress.reset(); + while let Some((tx, request)) = self.requests.recv().await { + trace!("Received request: {:?}", request); + let response = self.handle_message(request); + tx.send(response).unwrap() + } + debug!("No listeners remain, disconnecting"); + } + + fn handle_message(&mut self, request: MarketRequest) -> MarketResponse { + match request { + MarketRequest::Data { ticker, start, end } => { + let data = self.get_data(&ticker, start, end); + MarketResponse::Data(data) + } + MarketRequest::GetOpen { ticker } => MarketResponse::MaybePrice(self.get_open(&ticker)), + MarketRequest::GetCurrent { ticker } => { + MarketResponse::MaybePrice(self.get_current_price(&ticker)) + } + MarketRequest::GetLast { ticker } => { + MarketResponse::MaybePrice(self.get_last_price(&ticker)) + } + MarketRequest::Datetime => MarketResponse::Datetime(self.datetime()), + MarketRequest::PreviousDatetime => MarketResponse::Datetime(self.previous_datetime()), + MarketRequest::NextDatetime => MarketResponse::Datetime(self.next_datetime()), + MarketRequest::State => MarketResponse::State(self.state()), + MarketRequest::IsDone => MarketResponse::Bool(self.is_done()), + MarketRequest::IsOpen => MarketResponse::Bool(self.is_open()), + MarketRequest::Tick => { + self.tick(); + MarketResponse::Success + } + } + } + + #[tracing::instrument(skip(self))] + fn get_open(&self, ticker: &str) -> Option { + trace!(ticker, "Get open"); + let datetime = self.datetime(); + let data = self.get_data(ticker, datetime, datetime); + data.map(|x| x.first().unwrap().open) + } + + #[tracing::instrument(skip(self))] + fn get_current_price(&self, ticker: &str) -> Option { + trace!(ticker, "Get current price"); + let datetime = self.datetime(); + self.data_manager + .get_last_before(ticker, datetime) + .map(|x| x.close) + } + + #[tracing::instrument(skip(self))] + pub fn get_last_price(&self, ticker: &str) -> Option { + trace!(ticker, "Get last price"); + let datetime = self.previous_datetime(); + let data = self.get_data(ticker, datetime, datetime); + data.map(|x| x.last().unwrap().close) + } + + #[tracing::instrument(skip(self))] + fn get_data( + &self, + ticker: &str, + start: DateTime, + end: DateTime, + ) -> Option> { + trace!(ticker, %start, %end, "Get data"); + self.data_manager.get_data(ticker, start, end) + } + + #[tracing::instrument(skip(self))] + fn datetime(&self) -> DateTime { + self.clock.datetime() + } + + #[tracing::instrument(skip(self))] + fn state(&self) -> MarketState { + self.clock.state() + } + + #[tracing::instrument(skip(self))] + fn is_done(&self) -> bool { + if self.clock.is_done() { + self.progress.finish(); + true + } else { + false + } + } + + #[tracing::instrument(skip(self))] + fn is_open(&self) -> bool { + self.clock.is_open() + } + + #[tracing::instrument(skip(self))] + fn previous_datetime(&self) -> DateTime { + self.clock.previous_datetime() + } + + #[tracing::instrument(skip(self))] + fn next_datetime(&self) -> DateTime { + self.clock.next_datetime() + } + + #[tracing::instrument(skip(self))] + fn tick(&mut self) { + self.clock.tick(); + self.progress.inc(1); + } +} diff --git a/src/markets/clock.rs b/src/markets/clock.rs index f11ed9e..386f570 100644 --- a/src/markets/clock.rs +++ b/src/markets/clock.rs @@ -1,4 +1,13 @@ -use chrono::{DateTime, Utc}; +use crate::utils::nyse_calendar::NyseCalendar; +use crate::Resolution; +use bdays::{HolidayCalendar, HolidayCalendarCache}; +use chrono::{DateTime, Duration, NaiveDate, NaiveTime, TimeZone}; +use chrono_tz::{Tz, US::Eastern}; + +lazy_static! { + static ref OPENING_TIME: NaiveTime = NaiveTime::from_hms(9, 30, 0); + static ref CLOSING_TIME: NaiveTime = NaiveTime::from_hms(16, 0, 0); +} #[derive(Copy, Clone, Debug, PartialEq)] pub enum MarketState { @@ -22,49 +31,119 @@ impl MarketState { } #[derive(Clone)] -pub(crate) struct Clock { - idx: usize, +struct ClockOptions { + end: NaiveDate, + resolution: Resolution, +} + +pub struct Clock { + datetime: DateTime, market_state: MarketState, - timestamps: Vec>, + calendar: HolidayCalendarCache, + options: ClockOptions, } impl Clock { - pub fn new(timestamps: Vec>) -> Self { + pub fn new( + mut start: NaiveDate, + end: NaiveDate, + warmup: Duration, + resolution: Resolution, + ) -> Self { + let calendar = HolidayCalendarCache::new(NyseCalendar, start, end); + if !calendar.is_bday(start) { + start = calendar.advance_bdays(start, 1); + } + let datetime = Eastern + .from_local_datetime(&start.and_time(*OPENING_TIME)) + .unwrap() + + warmup; + let options = ClockOptions { end, resolution }; Self { - idx: 0, + datetime, market_state: MarketState::PreOpen, - timestamps, + calendar, + options, + } + } + + pub fn simulation_periods(&self) -> i32 { + let days = self + .calendar + .bdays(self.datetime.date().naive_local(), self.options.end); + match self.options.resolution { + Resolution::Day => days * 5, + Resolution::Minute => days * 395, } } - //pub fn datetime_offset(&self, n: usize) -> Option<&DateTime> { - // self.timestamps.get(self.idx - n) - //} + pub fn is_done(&self) -> bool { + (self.datetime.date().naive_local() >= self.options.end) + && self.market_state == MarketState::Closed + } + + pub fn is_start_of_day(&self) -> bool { + match self.options.resolution { + Resolution::Minute => self.datetime.time() == *OPENING_TIME, + // Since there's only one tick per day, it's always the end of the day + Resolution::Day => true, + } + } + + pub fn is_end_of_day(&self) -> bool { + // TODO: Fix for early closing time + match self.options.resolution { + Resolution::Minute => self.datetime.time() == *CLOSING_TIME, + // Since there's only one tick per day, it's always the end of the day + Resolution::Day => true, + } + } - pub fn previous_datetime(&self) -> Option<&DateTime> { - if self.idx == 0 { - None + pub fn previous_datetime(&self) -> DateTime { + if self.is_start_of_day() { + Eastern + .from_local_datetime( + &self + .calendar + .advance_bdays(self.datetime.date().naive_local(), -1) + // TODO: Fix for early closing time + .and_time(*CLOSING_TIME), + ) + .unwrap() } else { - self.timestamps.get(self.idx - 1) + match self.options.resolution { + Resolution::Minute => self.datetime - Duration::minutes(1), + Resolution::Day => unreachable!(), + } } } - pub fn datetime(&self) -> Option<&DateTime> { - self.timestamps.get(self.idx) + pub fn datetime(&self) -> DateTime { + self.datetime } - pub fn next_datetime(&self) -> Option<&DateTime> { - self.timestamps.get(self.idx + 1) + pub fn next_datetime(&self) -> DateTime { + if self.is_end_of_day() { + Eastern + .from_local_datetime( + &self + .calendar + .advance_bdays(self.datetime.date().naive_local(), 1) + .and_time(*OPENING_TIME), + ) + .unwrap() + } else { + match self.options.resolution { + Resolution::Minute => self.datetime + Duration::minutes(1), + Resolution::Day => unreachable!(), + } + } } pub fn state(&self) -> MarketState { self.market_state } - pub fn is_done(&self) -> bool { - (self.idx == (self.timestamps.len() - 1)) && self.state() == MarketState::Closed - } - pub fn is_open(&self) -> bool { match self.market_state { MarketState::Opening | MarketState::Open | MarketState::Closing => true, @@ -73,27 +152,34 @@ impl Clock { } pub fn tick(&mut self) { - let datetime = match self.datetime() { - Some(datetime) => datetime, - None => return, - }; - let next_datetime = self.next_datetime(); + if self.is_done() { + panic!("Market clock ticked after end of backtest"); + } + let state = &self.market_state; - if let Some(next_datetime) = next_datetime { - if datetime.date() != next_datetime.date() { - if let MarketState::Closed = state { - self.idx += 1 - } - self.market_state = state.next(); - } else { - match state { - MarketState::Opening | MarketState::Open => self.market_state = state.next(), - _ => self.idx += 1, - } + + if self.is_end_of_day() { + if let MarketState::Closed = state { + self.datetime = Eastern + .from_local_datetime( + &(self + .calendar + .advance_bdays(self.datetime.date().naive_local(), 1)) + .and_time(*OPENING_TIME), + ) + .unwrap(); } - } else if let MarketState::Closed = state { - } else { self.market_state = state.next(); + } else { + match state { + MarketState::PreOpen | MarketState::Opening => self.market_state = state.next(), + _ => match self.options.resolution { + Resolution::Minute => self.datetime = self.datetime + Duration::minutes(1), + // We should never reach the below as `self.is_end_of_day` should always be + // true for daily resolution + Resolution::Day => unreachable!(), + }, + } } } } @@ -101,12 +187,24 @@ impl Clock { #[cfg(test)] mod test { use super::*; + use chrono::Duration; #[test] fn it_can_tell_and_update_time() { - let mut clock = Clock::new(vec![Utc::now(), Utc::now() + chrono::Duration::days(1)]); - assert!(clock.datetime().is_some()); - assert!(clock.next_datetime().is_some()); + let mut clock = Clock::new( + NaiveDate::from_ymd(2021, 1, 1), + NaiveDate::from_ymd(2021, 12, 31), + Duration::zero(), + Resolution::Day, + ); + assert_eq!( + clock.datetime().naive_local(), + NaiveDate::from_ymd(2021, 1, 5).and_hms(9, 30, 0) + ); + assert_eq!( + clock.next_datetime().naive_local(), + NaiveDate::from_ymd(2021, 1, 6).and_hms(9, 30, 0) + ); assert_eq!(clock.state(), MarketState::PreOpen); assert!(!clock.is_open()); @@ -129,6 +227,56 @@ mod test { clock.tick(); assert_eq!(clock.state(), MarketState::PreOpen); - assert!(clock.next_datetime().is_none()); + } + + #[test] + fn it_works_for_intraday_data() { + let mut clock = Clock::new( + NaiveDate::from_ymd(2021, 1, 1), + NaiveDate::from_ymd(2021, 12, 31), + Duration::zero(), + Resolution::Minute, + ); + assert_eq!( + clock.datetime().naive_local(), + NaiveDate::from_ymd(2021, 1, 5).and_hms(9, 30, 0) + ); + assert_eq!( + clock.next_datetime().naive_local(), + NaiveDate::from_ymd(2021, 1, 5).and_hms(9, 31, 0) + ); + + assert_eq!(clock.state(), MarketState::PreOpen); + assert!(!clock.is_open()); + + clock.tick(); + assert_eq!(clock.state(), MarketState::Opening); + assert!(clock.is_open()); + + for _ in 0..391 { + clock.tick(); + assert_eq!(clock.state(), MarketState::Open); + assert!(clock.is_open()); + } + + clock.tick(); + assert_eq!(clock.state(), MarketState::Closing); + assert!(clock.is_open()); + + clock.tick(); + assert_eq!(clock.state(), MarketState::Closed); + assert!(!clock.is_open()); + + clock.tick(); + assert_eq!(clock.state(), MarketState::PreOpen); + assert!(!clock.is_open()); + + clock.tick(); + assert_eq!(clock.state(), MarketState::Opening); + assert!(clock.is_open()); + + clock.tick(); + assert_eq!(clock.state(), MarketState::Open); + assert!(clock.is_open()); } } diff --git a/src/markets/data_manager.rs b/src/markets/data_manager.rs new file mode 100644 index 0000000..fdf33c0 --- /dev/null +++ b/src/markets/data_manager.rs @@ -0,0 +1,113 @@ +use crate::data::Aggregate; +use crate::{Options, Resolution}; +use chrono::prelude::*; +use chrono_tz::{Tz, US::Eastern}; +use futures::{stream, StreamExt, TryStreamExt}; +use polygon::rest::{client, GetAggregate, Timespan}; +use std::collections::{BTreeMap, HashMap}; +use stream_flatten_iters::TryStreamExt as _; + +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct DownloadJob { + ticker: String, + start: NaiveDate, + end: NaiveDate, + resolution: Resolution, +} + +pub struct DataManager { + download_jobs: Vec, + data: HashMap, Aggregate>>, +} + +impl DataManager { + pub fn new(data_options: Options) -> Self { + let download_jobs = data_options + .tickers + .iter() + .map(|ticker| { + let timespan = match data_options.resolution { + Resolution::Day => Timespan::Day, + Resolution::Minute => Timespan::Minute, + }; + let start = data_options.start.and_hms(0, 0, 0); + let end = data_options.end.and_hms(0, 0, 0); + GetAggregate::new(ticker, start, end) + .timespan(timespan) + .limit(50000) + }) + .collect(); + + Self { + download_jobs, + data: HashMap::new(), + } + } + + pub async fn download_data(&mut self) { + let jobs = self.download_jobs.clone(); + let client = client(&std::env::var("POLYGON_TOKEN").expect( + "The Polygon data provider requires the POLYGON_TOKEN environment variable to be set", + )) + .show_progress(); + let data = stream::select_all(client.send_all_paginated(jobs.iter()).map(|stream| { + stream + .map_ok(|wrapper| { + let ticker = wrapper.ticker.clone(); + wrapper + .results + .into_iter() + .map(move |r| (ticker.clone(), r)) + }) + .try_flatten_iters() + })) + .filter_map(|x| async move { x.ok() }) + .map(|x| async { x }) + .buffer_unordered(500); + + let mut data = Box::pin(data); + while let Some((ticker, agg)) = data.next().await { + self.data + .entry(ticker.to_string()) + .and_modify(|map| { + let agg = agg.clone(); + map.insert(agg.t.with_timezone(&Eastern), From::from(agg)); + }) + .or_insert_with(|| { + let mut map = BTreeMap::new(); + map.insert(agg.t.with_timezone(&Eastern), From::from(agg)); + map + }); + } + } + + pub fn get_data( + &self, + ticker: &str, + start: DateTime, + end: DateTime, + ) -> Option> { + let data: Vec = self + .data + .get(ticker)? + .range(start..=end) + .map(|(_, agg)| agg) + .cloned() + .collect(); + if data.is_empty() { + None + } else { + Some(data) + } + } + + pub fn get_last_before(&self, ticker: &str, datetime: DateTime) -> Option { + let start = chrono::MIN_DATETIME.with_timezone(&datetime.timezone()); + self.data + .get(ticker)? + .range(start..=datetime) + .last() + .map(|(_, agg)| agg) + .cloned() + } +} diff --git a/src/markets/handle.rs b/src/markets/handle.rs new file mode 100644 index 0000000..d4c5570 --- /dev/null +++ b/src/markets/handle.rs @@ -0,0 +1,179 @@ +use crate::data::Aggregate; +use crate::markets::clock::MarketState; +use chrono::DateTime; +use chrono_tz::Tz; +use rust_decimal::Decimal; +use tokio::sync::mpsc::UnboundedSender; +use tokio::sync::oneshot::{self, Sender as OneshotSender}; + +#[derive(Clone, Debug)] +pub(crate) enum MarketRequest { + Datetime, + IsDone, + IsOpen, + Data { + ticker: String, + start: DateTime, + end: DateTime, + }, + GetOpen { + ticker: String, + }, + GetCurrent { + ticker: String, + }, + GetLast { + ticker: String, + }, + NextDatetime, + State, + PreviousDatetime, + Tick, +} + +#[derive(Clone, Debug)] +pub(crate) enum MarketResponse { + Bool(bool), + Data(Option>), + Datetime(DateTime), + MaybePrice(Option), + State(MarketState), + // Generic reply for when no reply is needed + Success, +} + +#[derive(Clone)] +pub struct Market { + sender: UnboundedSender<(OneshotSender, MarketRequest)>, +} + +impl Market { + pub(crate) fn new( + sender: UnboundedSender<(OneshotSender, MarketRequest)>, + ) -> Self { + Self { sender } + } + + async fn send_request(&self, request: MarketRequest) -> MarketResponse { + let (tx, rx) = oneshot::channel(); + self.sender.send((tx, request)).unwrap(); + rx.await.unwrap() + } + + pub async fn datetime(&self) -> DateTime { + let response = self.send_request(MarketRequest::Datetime).await; + if let MarketResponse::Datetime(dt) = response { + dt + } else { + unreachable!() + } + } + + pub async fn previous_datetime(&self) -> DateTime { + let response = self.send_request(MarketRequest::PreviousDatetime).await; + if let MarketResponse::Datetime(dt) = response { + dt + } else { + unreachable!() + } + } + + pub async fn next_datetime(&self) -> DateTime { + let response = self.send_request(MarketRequest::NextDatetime).await; + if let MarketResponse::Datetime(dt) = response { + dt + } else { + unreachable!() + } + } + + pub async fn state(&self) -> MarketState { + let response = self.send_request(MarketRequest::State).await; + if let MarketResponse::State(state) = response { + state + } else { + unreachable!() + } + } + + pub async fn is_open(&self) -> bool { + let response = self.send_request(MarketRequest::IsOpen).await; + if let MarketResponse::Bool(b) = response { + b + } else { + unreachable!() + } + } + + pub async fn get_data( + &self, + ticker: T, + start: DateTime, + end: DateTime, + ) -> Option> { + let response = self + .send_request(MarketRequest::Data { + ticker: ticker.to_string(), + start, + end, + }) + .await; + if let MarketResponse::Data(data) = response { + data + } else { + unreachable!() + } + } + + pub(crate) async fn get_current_price(&self, ticker: &str) -> Option { + let response = self + .send_request(MarketRequest::GetCurrent { + ticker: ticker.to_string(), + }) + .await; + if let MarketResponse::MaybePrice(p) = response { + p + } else { + unreachable!() + } + } + + pub async fn get_last_price(&self, ticker: &str) -> Option { + let response = self + .send_request(MarketRequest::GetLast { + ticker: ticker.to_string(), + }) + .await; + if let MarketResponse::MaybePrice(p) = response { + p + } else { + unreachable!() + } + } + + pub async fn get_open(&self, ticker: &str) -> Option { + let response = self + .send_request(MarketRequest::GetOpen { + ticker: ticker.to_string(), + }) + .await; + if let MarketResponse::MaybePrice(p) = response { + p + } else { + unreachable!() + } + } + + pub(crate) async fn is_done(&self) -> bool { + let response = self.send_request(MarketRequest::IsDone).await; + if let MarketResponse::Bool(b) = response { + b + } else { + unreachable!() + } + } + + pub(crate) async fn tick(&self) { + self.send_request(MarketRequest::Tick).await; + } +} diff --git a/src/markets/market.rs b/src/markets/market.rs deleted file mode 100644 index 9349dcd..0000000 --- a/src/markets/market.rs +++ /dev/null @@ -1,124 +0,0 @@ -use crate::data::{Aggregate, MarketData}; -use crate::markets::clock::{Clock, MarketState}; -use chrono::{DateTime, Utc}; -use rust_decimal::Decimal; -use std::cell::RefCell; -use std::collections::BTreeSet; -use std::rc::Rc; - -struct Inner { - clock: Clock, - data: MarketData, -} - -#[derive(Clone)] -pub struct Market { - inner: Rc>, -} - -impl Market { - pub fn new(data: MarketData) -> Self { - let timestamps: BTreeSet> = - data.prices.values().flatten().map(|x| *x.0).collect(); - let clock = Clock::new(timestamps.into_iter().collect()); - let inner = Rc::new(RefCell::new(Inner { clock, data })); - Self { inner } - } - - pub(crate) fn get_current_price(&self, ticker: &str) -> Option { - let inner = self.inner.borrow(); - let timeseries = inner.data.prices.get(ticker)?; - let state = inner.clock.state(); - match state { - MarketState::PreOpen | MarketState::Closed => None, - MarketState::Opening => { - let datetime = inner.clock.datetime()?; - timeseries.get(datetime).map(|agg| agg.open) - } - MarketState::Open | MarketState::Closing => { - let datetime = inner.clock.datetime()?; - timeseries.get(datetime).map(|agg| agg.close) - } - } - } - - pub fn get_last_price(&self, ticker: &str) -> Option { - let inner = self.inner.borrow(); - let timeseries = inner.data.prices.get(ticker)?; - let state = inner.clock.state(); - match state { - MarketState::PreOpen | MarketState::Opening => { - let previous_datetime = inner.clock.previous_datetime()?; - timeseries.get(previous_datetime).map(|agg| agg.close) - } - MarketState::Open => { - let datetime = inner.clock.datetime()?; - timeseries.get(datetime).map(|agg| agg.open) - } - MarketState::Closing | MarketState::Closed => { - let datetime = inner.clock.datetime()?; - timeseries.get(datetime).map(|agg| agg.close) - } - } - } - - pub fn get_last_aggregate(&self, ticker: &str) -> Option { - let inner = self.inner.borrow(); - let timeseries = inner.data.prices.get(ticker)?; - let state = inner.clock.state(); - match state { - MarketState::PreOpen - | MarketState::Opening - | MarketState::Open - | MarketState::Closing => { - let previous_datetime = inner.clock.previous_datetime()?; - timeseries.get(previous_datetime).cloned() - } - MarketState::Closed => { - let datetime = inner.clock.datetime()?; - timeseries.get(datetime).cloned() - } - } - } - - pub fn with_data( - &self, - ticker: &str, - start: &DateTime, - end: &DateTime, - f: F, - ) -> Option - where - F: Fn(Vec<&Aggregate>) -> T, - { - let inner = self.inner.borrow(); - let timeseries = inner.data.prices.get(ticker)?; - let data = timeseries.range(start..end).map(|(_, v)| v).collect(); - Some(f(data)) - } - - pub fn datetime(&self) -> DateTime { - *self - .inner - .borrow() - .clock - .datetime() - .expect("Should always be in range") - } - - pub fn state(&self) -> MarketState { - self.inner.borrow().clock.state() - } - - pub(crate) fn is_done(&self) -> bool { - self.inner.borrow().clock.is_done() - } - - pub fn is_open(&self) -> bool { - self.inner.borrow().clock.is_open() - } - - pub(crate) fn tick(&self) { - self.inner.borrow_mut().clock.tick() - } -} diff --git a/src/markets/mod.rs b/src/markets/mod.rs index 4e76dfd..cecc480 100644 --- a/src/markets/mod.rs +++ b/src/markets/mod.rs @@ -1,2 +1,4 @@ +pub(crate) mod actor; pub(crate) mod clock; -pub(crate) mod market; +pub(crate) mod data_manager; +pub(crate) mod handle; diff --git a/src/options.rs b/src/options.rs new file mode 100644 index 0000000..0d96106 --- /dev/null +++ b/src/options.rs @@ -0,0 +1,56 @@ +use chrono::{Duration, NaiveDate}; +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, DurationSeconds}; + +#[derive(Deserialize, Serialize, Debug, Clone, Copy, Eq, Hash, PartialEq)] +pub enum Resolution { + Minute, + Day, +} + +#[serde_as] +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct Options { + pub tickers: Vec, + pub start: NaiveDate, + pub end: NaiveDate, + #[serde_as(as = "DurationSeconds")] + pub warmup: Duration, + pub resolution: Resolution, + pub normalize: bool, + pub outdir: Option, +} + +impl Options { + pub fn new(tickers: Vec, start: NaiveDate, end: NaiveDate) -> Self { + Self { + tickers, + start, + end, + warmup: Duration::zero(), + resolution: Resolution::Day, + normalize: false, + outdir: None, + } + } + + pub fn set_resolution(mut self, resolution: Resolution) -> Self { + self.resolution = resolution; + self + } + + pub fn set_warmup(mut self, warmup: Duration) -> Self { + self.warmup = warmup; + self + } + + pub fn set_normalize(mut self, normalize: bool) -> Self { + self.normalize = normalize; + self + } + + pub fn set_outdir(mut self, outdir: T) -> Self { + self.outdir = Some(outdir.to_string()); + self + } +} diff --git a/src/simulator.rs b/src/simulator.rs index 7225b11..65df3d9 100644 --- a/src/simulator.rs +++ b/src/simulator.rs @@ -1,60 +1,96 @@ -use crate::brokerage::{order::Order, Brokerage, Event, OrderStatus}; -use crate::markets::{clock::MarketState, market::Market}; +use crate::brokerage::{ + actor::{BrokerageActor, Event}, + handle::Brokerage, + order::{Order, OrderStatus}, +}; +use crate::markets::{actor::MarketActor, clock::MarketState, handle::Market}; use crate::statistics::Statistics; use crate::strategy::Strategy; -use chrono::{DateTime, Utc}; -use std::sync::mpsc::Receiver; +use crate::Options; +use chrono::DateTime; +use chrono_tz::Tz; +use rust_decimal::Decimal; +use std::fs::{create_dir_all, remove_file, OpenOptions}; +use std::io::Write; +use tracing::{trace, Instrument}; -const CLEAR_SCREEN: &str = "\x1B[2J\x1B[1;1H"; - -pub struct Simulator { +pub struct Simulator { brokerage: Brokerage, market: Market, strategy: S, statistics: Statistics, - event_listener: Receiver, + data_options: Options, } -impl Simulator { - pub fn new(mut brokerage: Brokerage, strategy: S) -> Self { - let market = brokerage.get_market(); - let event_listener = brokerage.subscribe(); +impl Simulator { + pub fn new(cash: Decimal, strategy: S, data_options: Options) -> Self { + let market = MarketActor::spawn(data_options.clone()); + let brokerage = BrokerageActor::spawn(cash, market.clone()); let statistics = Statistics::new(); Self { brokerage, market, strategy, statistics, - event_listener, + data_options, } } -} -impl Simulator { - pub fn run(&mut self) -> Result<(), S::Error> { - self.strategy.initialize(); - while !self.market.is_done() { - print!("{}", CLEAR_SCREEN); - println!("{}", self.market.datetime()); - match self.market.state() { - MarketState::PreOpen => { - self.strategy.before_open(&mut self.brokerage, &self.market) - } - MarketState::Opening => self.strategy.at_open(&mut self.brokerage, &self.market), - MarketState::Open => { - self.brokerage.reconcile_active_orders(); - self.strategy - .during_regular_hours(&mut self.brokerage, &self.market) + pub async fn run(mut self) -> Result<(), S::Error> { + self.strategy.initialize().await; + let mut event_listener = self.brokerage.subscribe().await; + while !self.market.is_done().await { + let (datetime, state) = futures::join!(self.market.datetime(), self.market.state()); + let span = tracing::debug_span!("Datetime", %datetime, ?state); + async { + match state { + MarketState::PreOpen => { + self.strategy + .before_open(self.brokerage.clone(), self.market.clone()) + .instrument(tracing::trace_span!("Before open")) + .await + } + MarketState::Opening => { + self.strategy + .at_open(self.brokerage.clone(), self.market.clone()) + .instrument(tracing::trace_span!("At open")) + .await + } + MarketState::Open => { + self.brokerage.reconcile_active_orders().await; + self.strategy + .during_regular_hours(self.brokerage.clone(), self.market.clone()) + .instrument(tracing::trace_span!("Regular hours")) + .await + } + MarketState::Closing => { + self.strategy + .at_close(self.brokerage.clone(), self.market.clone()) + .instrument(tracing::trace_span!("At close")) + .await + } + MarketState::Closed => { + self.brokerage.expire_orders().await; + self.strategy + .after_close(self.brokerage.clone(), self.market.clone()) + .instrument(tracing::trace_span!("After close")) + .await?; + Ok(()) + } + }?; + while let Ok(event) = event_listener.try_recv() { + trace!("Event received: {:?}", event); + self.strategy.on_event(event.clone()).await?; + self.handle_event(event) } - MarketState::Closing => self.strategy.at_close(&mut self.brokerage, &self.market), - MarketState::Closed => self.strategy.after_close(&mut self.brokerage, &self.market), - }?; - while let Ok(event) = self.event_listener.try_recv() { - self.strategy.on_event(event.clone())?; - self.handle_event(event) + let equity = self.brokerage.get_equity().await; + trace!("Equity: {:.2}", equity); + self.statistics.record_equity(datetime, equity); + self.market.tick().await; + Ok(()) } - self.statistics.record_equity(self.brokerage.get_equity()); - self.market.tick(); + .instrument(span) + .await? } self.generate_report(); Ok(()) @@ -72,11 +108,53 @@ impl Simulator { } } - fn handle_order_update(&mut self, status: OrderStatus, _order: Order, _time: DateTime) { + fn handle_order_update(&mut self, status: OrderStatus, _order: Order, _time: DateTime) { self.statistics.handle_order(&status) } - pub fn generate_report(&self) { - println!("{}", self.statistics) + pub fn generate_report(self) { + let outdir = self + .data_options + .outdir + .unwrap_or_else(|| "out".to_string()); + let _ = create_dir_all(outdir.clone()); + let filename = format!("{}/statistics.txt", outdir); + let _ = remove_file(filename.clone()); + let mut file = OpenOptions::new() + .create(true) + .write(true) + .open(filename) + .unwrap(); + write!(file, "{}", self.statistics).unwrap(); + file.flush().unwrap(); + + let filename = format!("{}/equity.csv", outdir); + let _ = remove_file(filename.clone()); + let file = OpenOptions::new() + .create(true) + .write(true) + .open(filename) + .unwrap(); + let mut wtr = csv::Writer::from_writer(file); + wtr.write_record(&["datetime", "equity"]).unwrap(); + for (d, e) in self.statistics.equity { + wtr.write_record(&[d.to_string(), e.to_string()]).unwrap() + } + wtr.flush().unwrap(); + + let filename = format!("{}/event_log.json", outdir); + let _ = remove_file(filename.clone()); + let mut file = OpenOptions::new() + .create(true) + .write(true) + .open(filename) + .unwrap(); + write!( + file, + "{}", + serde_json::to_string_pretty(&self.statistics.event_log).unwrap() + ) + .unwrap(); + file.flush().unwrap(); } } diff --git a/src/statistics.rs b/src/statistics.rs index 462a7f4..165d3a8 100644 --- a/src/statistics.rs +++ b/src/statistics.rs @@ -1,4 +1,7 @@ -use crate::brokerage::{Event, OrderStatus}; +use crate::brokerage::actor::Event; +use crate::brokerage::order::OrderStatus; +use chrono::DateTime; +use chrono_tz::Tz; use rust_decimal::Decimal; use std::fmt; @@ -8,14 +11,21 @@ pub struct OrderCounts { cancelled: usize, filled: usize, rejected: usize, + expired: usize, } impl fmt::Display for OrderCounts { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let max = vec![self.submitted, self.cancelled, self.filled, self.rejected] - .into_iter() - .max() - .unwrap(); + let max = vec![ + self.submitted, + self.cancelled, + self.filled, + self.rejected, + self.expired, + ] + .into_iter() + .max() + .unwrap(); let digits = f64::log10(max as f64).ceil() as usize; let full_digits = digits + 11; write!( @@ -28,6 +38,7 @@ Submitted: {:>digits$} Cancelled: {:>digits$} Filled: {:>digits$} Rejected: {:>digits$} +Expired: {:>digits$} "#, "", "Orders", @@ -36,6 +47,7 @@ Rejected: {:>digits$} self.cancelled, self.filled, self.rejected, + self.expired, full_digits = full_digits, digits = digits ) @@ -46,8 +58,8 @@ Rejected: {:>digits$} pub struct Statistics { order_counts: OrderCounts, commission_paid: Decimal, - equity: Vec, - event_log: Vec, + pub equity: Vec<(DateTime, Decimal)>, + pub event_log: Vec, } impl Statistics { @@ -70,12 +82,13 @@ impl Statistics { OrderStatus::Cancelled => self.order_counts.cancelled += 1, OrderStatus::Filled { .. } => self.order_counts.filled += 1, OrderStatus::Rejected => self.order_counts.rejected += 1, + OrderStatus::Expired => self.order_counts.expired += 1, OrderStatus::PartiallyFilled => (), } } - pub fn record_equity(&mut self, equity: Decimal) { - self.equity.push(equity) + pub fn record_equity(&mut self, datetime: DateTime, equity: Decimal) { + self.equity.push((datetime, equity)); } pub fn increase_commission(&mut self, amount: Decimal) { @@ -83,16 +96,26 @@ impl Statistics { } pub fn max_drawdown(&self) -> Decimal { + #[derive(Default)] + struct State { + max_equity: Decimal, + max_drawdown: Decimal, + } + self.equity .iter() - .fold((Decimal::ZERO, Decimal::ZERO), |mut state, equity| { - if equity > &state.0 { - state.0 = *equity + .map(|(_, e)| e) + .fold(State::default(), |mut state, equity| { + if equity > &state.max_equity { + state.max_equity = *equity }; - state.1 = equity / state.0 - Decimal::ONE; + let drawdown = equity / state.max_equity - Decimal::ONE; + if drawdown < state.max_drawdown { + state.max_drawdown = drawdown; + } state }) - .1 + .max_drawdown } } @@ -121,14 +144,27 @@ Max: {:>.2} Min: {:>.2} Ending: {:>.2} "#, - self.equity.first().unwrap().round_dp(2), - self.equity.iter().max().unwrap().round_dp(2), - self.equity.iter().min().unwrap().round_dp(2), - self.equity.last().unwrap().round_dp(2), + self.equity.first().unwrap().1.round_dp(2), + self.equity.iter().map(|x| x.1).max().unwrap().round_dp(2), + self.equity.iter().map(|x| x.1).min().unwrap().round_dp(2), + self.equity.last().unwrap().1.round_dp(2), )?; write!( f, r#" +=============== + Returns +=============== +Total: {:>.2}% + "#, + (((self.equity.last().unwrap().1 / self.equity.first().unwrap().1) - Decimal::ONE) + * Decimal::new(100, 0)) + .round_dp(2) + ) + .unwrap(); + write!( + f, + r#" =============== Drawdowns =============== diff --git a/src/strategy.rs b/src/strategy.rs index 0142de0..5f1cf74 100644 --- a/src/strategy.rs +++ b/src/strategy.rs @@ -1,38 +1,46 @@ -use crate::brokerage::{Brokerage, Event}; -use crate::markets::market::Market; +use crate::brokerage::actor::Event; +use crate::brokerage::handle::Brokerage; +use crate::markets::handle::Market; +#[async_trait::async_trait] #[allow(unused_variables)] pub trait Strategy { type Error; - fn initialize(&mut self) {} - fn before_open( + async fn initialize(&mut self) {} + + async fn before_open( &mut self, - brokerage: &mut Brokerage, - market: &Market, + brokerage: Brokerage, + market: Market, ) -> Result<(), Self::Error> { Ok(()) } - fn on_event(&mut self, event: Event) -> Result<(), Self::Error> { + + async fn on_event(&mut self, event: Event) -> Result<(), Self::Error> { Ok(()) } - fn at_open(&mut self, brokerage: &mut Brokerage, market: &Market) -> Result<(), Self::Error> { + + async fn at_open(&mut self, brokerage: Brokerage, market: Market) -> Result<(), Self::Error> { Ok(()) } - fn during_regular_hours( + + async fn during_regular_hours( &mut self, - brokerage: &mut Brokerage, - market: &Market, + brokerage: Brokerage, + market: Market, ) -> Result<(), Self::Error> { Ok(()) } - fn at_close(&mut self, brokerage: &mut Brokerage, market: &Market) -> Result<(), Self::Error> { + + async fn at_close(&mut self, brokerage: Brokerage, market: Market) -> Result<(), Self::Error> { Ok(()) } - fn after_close( + + async fn after_close( &mut self, - brokerage: &mut Brokerage, - market: &Market, + brokerage: Brokerage, + market: Market, ) -> Result<(), Self::Error> { Ok(()) } diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..e9e0330 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,3 @@ +pub mod nyse_calendar; +pub mod progress; +pub mod serde_tz; diff --git a/src/utils/nyse_calendar.rs b/src/utils/nyse_calendar.rs new file mode 100644 index 0000000..05dd4ae --- /dev/null +++ b/src/utils/nyse_calendar.rs @@ -0,0 +1,271 @@ +use bdays::{easter::easter_naive_date, HolidayCalendar}; +use chrono::{Datelike, Duration, NaiveDate, Weekday}; + +fn end_of_month(mut yy: i32, mut mm: u32) -> NaiveDate { + assert!(mm <= 12); + + if mm == 12 { + yy += 1; + mm = 1; + } else { + mm += 1; + } + + NaiveDate::from_ymd(yy, mm, 1).pred() +} + +fn find_weekday_ascending(weekday: Weekday, yy: i32, mm: u32, occurrence: u32) -> NaiveDate { + let anchor = NaiveDate::from_ymd(yy, mm, 1); + let mut offset = (weekday.number_from_monday() + 7 - anchor.weekday().number_from_monday()) % 7; + + if occurrence > 1 { + offset += 7 * (occurrence - 1); + } + + anchor + Duration::days(offset as i64) +} + +fn find_weekday_descending(weekday: Weekday, yy: i32, mm: u32, occurrence: u32) -> NaiveDate { + let anchor = end_of_month(yy, mm); + let mut offset = (anchor.weekday().number_from_monday() + 7 - weekday.number_from_monday()) % 7; + + if occurrence > 1 { + offset += 7 * (occurrence - 1); + } + + anchor - Duration::days(offset as i64) +} + +fn find_weekday(weekday: Weekday, yy: i32, mm: u32, occurrence: u32, ascending: bool) -> NaiveDate { + if ascending { + find_weekday_ascending(weekday, yy, mm, occurrence) + } else { + find_weekday_descending(weekday, yy, mm, occurrence) + } +} + +/// In the United States, if a holiday falls on Saturday, it's observed on the preceding Friday. +/// If it falls on Sunday, it's observed on the next Monday. +fn adjust_weekend_holidays_us(date: NaiveDate) -> NaiveDate { + match date.weekday() { + Weekday::Sat => date - Duration::days(1), + Weekday::Sun => date + Duration::days(1), + _ => date, + } +} + +pub struct NyseCalendar; + +impl HolidayCalendar for NyseCalendar { + fn is_holiday(&self, date: T) -> bool { + let (yy, mm, dd) = (date.year(), date.month(), date.day()); + let dt_naive = NaiveDate::from_ymd(yy, mm, dd); + + // New Year's Day + if adjust_weekend_holidays_us(NaiveDate::from_ymd(yy, 1, 1)) == dt_naive { + return true; + } + + // Birthday of Martin Luther King, Jr. + if yy >= 1998 + && adjust_weekend_holidays_us(find_weekday(Weekday::Mon, yy, 1, 3, true)) == dt_naive + { + return true; + } + + // Washington's Birthday + if adjust_weekend_holidays_us(find_weekday(Weekday::Mon, yy, 2, 3, true)) == dt_naive { + return true; + } + + // Good Friday + let easter = easter_naive_date(yy).unwrap(); + if (easter - Duration::days(2)) == dt_naive { + return true; + } + + // Memorial Day + if adjust_weekend_holidays_us(find_weekday(Weekday::Mon, yy, 5, 1, false)) == dt_naive { + return true; + } + + // Juneteenth + if yy >= 2022 && adjust_weekend_holidays_us(NaiveDate::from_ymd(yy, 6, 19)) == dt_naive { + return true; + } + + // Independence Day + if adjust_weekend_holidays_us(NaiveDate::from_ymd(yy, 7, 4)) == dt_naive { + return true; + } + + // Labor Day + if adjust_weekend_holidays_us(find_weekday(Weekday::Mon, yy, 9, 1, true)) == dt_naive { + return true; + } + + // Thanksgiving Day + if adjust_weekend_holidays_us(find_weekday(Weekday::Thu, yy, 11, 4, true)) == dt_naive { + return true; + } + + // Christmas + if adjust_weekend_holidays_us(NaiveDate::from_ymd(yy, 12, 25)) == dt_naive { + return true; + } + + // Presidential election days + if (yy <= 1968 || (yy <= 1980 && yy % 4 == 0)) + && mm == 11 + && dd <= 7 + && dt_naive.weekday() == Weekday::Tue + { + return true; + } + + // Special closures + let special_closures = [ + // George H.W. Bush's funeral + NaiveDate::from_ymd(2018, 12, 5), + // Hurrican Sandy + NaiveDate::from_ymd(2012, 10, 29), + NaiveDate::from_ymd(2012, 10, 30), + // President Ford's funeral + NaiveDate::from_ymd(2007, 1, 2), + // 9/11 + NaiveDate::from_ymd(2001, 9, 11), + NaiveDate::from_ymd(2001, 9, 12), + NaiveDate::from_ymd(2001, 9, 13), + NaiveDate::from_ymd(2001, 9, 14), + // President Nixon's funeral + NaiveDate::from_ymd(1994, 4, 27), + // Hurrican Gloria + NaiveDate::from_ymd(1985, 9, 27), + // 1977 Blackout + NaiveDate::from_ymd(1977, 7, 14), + // President Johnson's funeral + NaiveDate::from_ymd(1973, 1, 25), + // President Truman's funeral + NaiveDate::from_ymd(1972, 12, 25), + // Moon landing + NaiveDate::from_ymd(1969, 7, 21), + // President Eisenhower's funeral + NaiveDate::from_ymd(1969, 3, 31), + // Heavy snow + NaiveDate::from_ymd(1969, 2, 10), + // Day after Independence day + NaiveDate::from_ymd(1968, 7, 5), + // Paperwork crisis + NaiveDate::from_ymd(1968, 6, 12), + NaiveDate::from_ymd(1968, 6, 19), + NaiveDate::from_ymd(1968, 6, 26), + NaiveDate::from_ymd(1968, 7, 3), + NaiveDate::from_ymd(1968, 7, 10), + NaiveDate::from_ymd(1968, 7, 17), + NaiveDate::from_ymd(1968, 7, 24), + NaiveDate::from_ymd(1968, 7, 31), + NaiveDate::from_ymd(1968, 8, 7), + NaiveDate::from_ymd(1968, 8, 14), + NaiveDate::from_ymd(1968, 8, 21), + NaiveDate::from_ymd(1968, 8, 28), + NaiveDate::from_ymd(1968, 9, 4), + NaiveDate::from_ymd(1968, 9, 11), + NaiveDate::from_ymd(1968, 9, 18), + NaiveDate::from_ymd(1968, 9, 25), + NaiveDate::from_ymd(1968, 10, 2), + NaiveDate::from_ymd(1968, 10, 9), + NaiveDate::from_ymd(1968, 10, 16), + NaiveDate::from_ymd(1968, 10, 23), + NaiveDate::from_ymd(1968, 10, 30), + NaiveDate::from_ymd(1968, 11, 6), + NaiveDate::from_ymd(1968, 11, 13), + NaiveDate::from_ymd(1968, 11, 20), + NaiveDate::from_ymd(1968, 11, 27), + NaiveDate::from_ymd(1968, 12, 4), + NaiveDate::from_ymd(1968, 12, 11), + NaiveDate::from_ymd(1968, 12, 18), + NaiveDate::from_ymd(1968, 12, 25), + // MLK assassination + NaiveDate::from_ymd(1968, 4, 9), + // President Kennedy's funeral + NaiveDate::from_ymd(1963, 11, 25), + // Day before Decoration day + NaiveDate::from_ymd(1961, 5, 29), + // Day after Christmas + NaiveDate::from_ymd(1958, 12, 26), + // Christmas eve + NaiveDate::from_ymd(1965, 12, 24), + NaiveDate::from_ymd(1956, 12, 24), + NaiveDate::from_ymd(1954, 12, 24), + ]; + + if special_closures.contains(&dt_naive) { + return true; + } + + false + } +} + +#[cfg(test)] +mod test { + // https://www.nyse.com/markets/hours-calendars + use super::*; + + const CAL: NyseCalendar = NyseCalendar; + lazy_static! { + static ref HOLIDAYS: [NaiveDate; 28] = [ + // New Years + NaiveDate::from_ymd(2021, 1, 1), + NaiveDate::from_ymd(2023, 1, 2), + // MLK + NaiveDate::from_ymd(2021, 1, 18), + NaiveDate::from_ymd(2022, 1, 17), + NaiveDate::from_ymd(2023, 1, 16), + // Presidents' day + NaiveDate::from_ymd(2021, 2, 15), + NaiveDate::from_ymd(2022, 2, 21), + NaiveDate::from_ymd(2023, 2, 20), + // Good Friday + NaiveDate::from_ymd(2021, 4, 2), + NaiveDate::from_ymd(2022, 4, 15), + NaiveDate::from_ymd(2023, 4, 7), + // Memorial Day + NaiveDate::from_ymd(2021, 5, 31), + NaiveDate::from_ymd(2022, 5, 30), + NaiveDate::from_ymd(2023, 5, 29), + // Juneteenth + NaiveDate::from_ymd(2022, 6, 20), + NaiveDate::from_ymd(2023, 6, 19), + // Independence Day + NaiveDate::from_ymd(2021, 7, 5), + NaiveDate::from_ymd(2022, 7, 4), + NaiveDate::from_ymd(2023, 7, 4), + // Labor day + NaiveDate::from_ymd(2021, 9, 6), + NaiveDate::from_ymd(2022, 9, 5), + NaiveDate::from_ymd(2023, 9, 4), + // Thanksgiving + NaiveDate::from_ymd(2021, 11, 25), + NaiveDate::from_ymd(2022, 11, 24), + NaiveDate::from_ymd(2023, 11, 23), + // Christmas Day + NaiveDate::from_ymd(2021, 12, 24), + NaiveDate::from_ymd(2022, 12, 26), + NaiveDate::from_ymd(2023, 12, 25), + ]; + } + + #[test] + fn holidays() { + let mut date = NaiveDate::from_ymd(2021, 1, 1); + while date < NaiveDate::from_ymd(2024, 1, 1) { + if HOLIDAYS.contains(&date) { + assert!(CAL.is_holiday(date), "{} is not a holiday", date) + } else { + assert!(!CAL.is_holiday(date), "{} is a holiday", date) + } + date += Duration::days(1) + } + } +} diff --git a/src/utils/progress.rs b/src/utils/progress.rs new file mode 100644 index 0000000..6f7d4ab --- /dev/null +++ b/src/utils/progress.rs @@ -0,0 +1,8 @@ +use indicatif::{ProgressBar, ProgressStyle}; + +pub fn progress(len: u64, message: &'static str) -> ProgressBar { + ProgressBar::new(len).with_message(message).with_style( + ProgressStyle::default_bar() + .template("[{elapsed_precise}] {bar} {pos:>7}/{len:7} [{eta}] {msg}"), + ) +} diff --git a/src/utils/serde_tz.rs b/src/utils/serde_tz.rs new file mode 100644 index 0000000..bb34593 --- /dev/null +++ b/src/utils/serde_tz.rs @@ -0,0 +1,17 @@ +use chrono::serde::ts_seconds; +use chrono::{DateTime, Utc}; +use chrono_tz::{Tz, US::Eastern}; +use serde::{Deserializer, Serializer}; + +pub fn deserialize<'de, D>(d: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + ts_seconds::deserialize(d).map(|res| res.with_timezone(&Eastern)) +} +pub fn serialize(dt: &DateTime, serializer: S) -> Result +where + S: Serializer, +{ + ts_seconds::serialize(&dt.with_timezone(&Utc), serializer) +}