Compare commits
714 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e3f1028667 | |||
| ea58ab2166 | |||
| 3b1aa2ebcb | |||
| fda2e70456 | |||
| 3cea91467d | |||
| dd6b37be16 | |||
| 078839b0f3 | |||
| 326438aac9 | |||
| 30da6eaead | |||
| 3784246e6b | |||
| fa6f189cdd | |||
| daa9d8d341 | |||
| c85afb59ab | |||
| 7d293f092e | |||
| da27204a71 | |||
| bac721e593 | |||
| bc666c6433 | |||
| 4a71992752 | |||
| 1b78186c4e | |||
| cb97aa193d | |||
| a02ac6e853 | |||
| f3cc790659 | |||
| 5ffbf76565 | |||
| 04b45adf98 | |||
| 9714c051c5 | |||
| fbf5a6d61a | |||
| 94632f42d6 | |||
| 3f6017d98b | |||
| d5af51c02b | |||
| 1ec0bb65f7 | |||
| c934a948c6 | |||
| 950bcaa7aa | |||
| 728ed7998d | |||
| 757b5665ca | |||
| c280bbc1c3 | |||
| 68c186f649 | |||
| f2de8b281a | |||
| 19f725dbaa | |||
| df7a1ee382 | |||
| 4fe1f0d77c | |||
| f45b3a71fa | |||
| 1aa7c28616 | |||
| e542e52bdb | |||
| 0afbdc2210 | |||
| fc6dea12eb | |||
| c3f88ebf12 | |||
| 1d1c93f8e2 | |||
| 929b105173 | |||
| e00777ebc2 | |||
| e4f6cdd025 | |||
| 2a705a361a | |||
| b4ec5da808 | |||
| 4d8761cc3c | |||
| fdd0dd0525 | |||
| df29c8d3ec | |||
| ef24402747 | |||
| 78e872b609 | |||
| 27e523a1dc | |||
| f9d9f06d3f | |||
| d45f39f1bf | |||
| 5fa3d3bf54 | |||
| 7bc832f3c6 | |||
| 46c647d676 | |||
| 24b961ba61 | |||
| d2cf021194 | |||
| 0542a44afe | |||
| 3f14d9de54 | |||
| b8e0d3bf4c | |||
| f1a6a2695d | |||
| 54ccfe7e0a | |||
| ce041e0d16 | |||
| 81f6b7d1a5 | |||
| dd376e4b32 | |||
| f927ef8c0f | |||
| 0d831c4ca6 | |||
| 96d0cc6fc4 | |||
| c7d213bf35 | |||
| b5934085da | |||
| 3657d83833 | |||
| 94a1422a35 | |||
| 5bc9753766 | |||
| a838732e5a | |||
| 4b88defdab | |||
| bc228980af | |||
| e0b4a3f56c | |||
| 840473362d | |||
| 630a7a16c9 | |||
| f5de3a2c29 | |||
| c5767ca272 | |||
| a7f96c030f | |||
| 5a7c49a454 | |||
| cda249e9b4 | |||
| 52c7831bf6 | |||
| 5122c73aa8 | |||
| 5f08978827 | |||
| 2135a5bd03 | |||
| 199f988b2e | |||
| 0f1d49bf63 | |||
| c7f5188354 | |||
| 6a1c60b2e2 | |||
| 2aa330786a | |||
| b65b3ed8f1 | |||
| f9ec86436a | |||
| 42999ffa2c | |||
| e060af380e | |||
| b55ac0bf81 | |||
| 6b5d57675e | |||
| c225d8484f | |||
| 15c8f1f403 | |||
| 52f9ee9d92 | |||
| d7e11be01f | |||
| 866fe32a8c | |||
| d89dd24f0a | |||
| 6a304e2fc2 | |||
| b2da70897a | |||
| 12ad2b163a | |||
| 1e8e738eda | |||
| 7f7fce1fc3 | |||
| a2d9e910ff | |||
| 29faf7932e | |||
| 0af4cd7f68 | |||
| 0e3c2028d0 | |||
| c2e9c11a9a | |||
| 95503d418c | |||
| 0c2681e699 | |||
| ab87e18ba5 | |||
| 33808d9ba9 | |||
| 1f8a0fdc44 | |||
| 229ca8332d | |||
| 1b449287fd | |||
| 05085b961b | |||
| cfb19b5783 | |||
| 6f1e40754d | |||
| d79678d850 | |||
| 224adf70d4 | |||
| f2105399c5 | |||
| eb8b14602a | |||
| df52f90d46 | |||
| 05e67c73df | |||
| 32e4030442 | |||
| be2f624e3c | |||
| 75ab0c06c6 | |||
| 1410aa588a | |||
| 43b15b2098 | |||
| 004a46cb41 | |||
| 76b1d6c38b | |||
| abe4fed452 | |||
| a35137ed1c | |||
| 7f17fa1fb4 | |||
| 5f7321da58 | |||
| e5646ef80c | |||
| 71b3517003 | |||
| 6f81e1940a | |||
| 98a74f7bb4 | |||
| 5b2a03d416 | |||
| e469b0ddf4 | |||
| 1f02e78b24 | |||
| a235f46b6d | |||
| 05953f857a | |||
| 6538742dfa | |||
| 1f10a6bb3d | |||
| 1df0081479 | |||
| db155e7ad0 | |||
| 6d458dbbc6 | |||
| f7708331d3 | |||
| 873cdb9d31 | |||
| 52b07be19b | |||
| 914820cc07 | |||
| 47d1520d72 | |||
| 8ed22897ea | |||
| 3963648f1a | |||
| 2c711871f5 | |||
| 836f56af41 | |||
| e27375a204 | |||
| 92322349d4 | |||
| 1a79dc483c | |||
| a3a077fa87 | |||
| b15b93eec1 | |||
| 1215a04b7d | |||
| d014bb2fbd | |||
| dff791b51f | |||
| ce46e42ff7 | |||
| cf1b60c996 | |||
| d0ca09d4d8 | |||
| c1e66b31a5 | |||
| cb18a3f279 | |||
| b634540047 | |||
| cd116bc3ae | |||
| f839b16dbd | |||
| 70d72d5d02 | |||
| f3a37157fc | |||
| ce5564408d | |||
| 41854fe5bd | |||
| d5265619f3 | |||
| e793a5fcc1 | |||
| 362d85773a | |||
| c55caaf79b | |||
| 257088d7d9 | |||
| ff2844884d | |||
| 56a38accc8 | |||
| de1e65a642 | |||
| 2d516cf1eb | |||
| e261d93ce5 | |||
| c1e7fc91e4 | |||
| 56511f9d15 | |||
| 8691076cef | |||
| 2de84e88eb | |||
| 43184a7afe | |||
| 4f647ef529 | |||
| 59be3b5dd5 | |||
| 5bbb093f47 | |||
| 520409f50f | |||
| e677a73f95 | |||
| 096115bd94 | |||
| cb2319cd22 | |||
| 732c3cfbf5 | |||
| 3d2ab17d72 | |||
| 87c18574a8 | |||
| c46dcf817b | |||
| 3f67ee9134 | |||
| 1a2e247c8d | |||
| 6c47c33cab | |||
| a9511f9a4a | |||
| 85c38b0756 | |||
| dd08358f1c | |||
| b6f4b05844 | |||
| c86a0e2339 | |||
| ed126a3630 | |||
| 45cafa5c96 | |||
| d0639558b3 | |||
| 8dccf8414f | |||
| d84fad721d | |||
| ce4d17c293 | |||
| 97e9a83dd2 | |||
| 9db2a9f79c | |||
| 801a27dace | |||
| 00ec7a6d55 | |||
| 9b00a0777a | |||
| 5a765766e3 | |||
| 6ee1835ca5 | |||
| cdbd8a67de | |||
| 8d7bb60c30 | |||
| 21cfa65af2 | |||
| 01d1e5073d | |||
| 5c300f8181 | |||
| 78c5b72ed3 | |||
| 32b3122457 | |||
| 50690540cd | |||
| 71b4ffcecf | |||
| 7f66bcfff7 | |||
| 422e5f1021 | |||
| 68bff3f661 | |||
| b92a09b5ed | |||
| e867aea3b1 | |||
| 32693dad30 | |||
| 723ce93623 | |||
| b05bd3fc1b | |||
| 48acd66c30 | |||
| 861ed483b5 | |||
| 3926ac2985 | |||
| a72a4518d3 | |||
| 255fc2b878 | |||
| dbaf7b45af | |||
| 8e238f9131 | |||
| 88f5a1a0ae | |||
| bc0f5f6089 | |||
| 0793473210 | |||
| 8303a6477b | |||
| 935dd47e34 | |||
| 8fcf34b3a9 | |||
| c18306aed9 | |||
| 9399a61b46 | |||
| 33370fb694 | |||
| 82604481b6 | |||
| 283bf56e9f | |||
| d3bacffd8b | |||
| 670686681d | |||
| 5929fedf91 | |||
| 8b246a63f0 | |||
| 0867655d85 | |||
| 782bc640ff | |||
| 77622e164c | |||
| 6604afd24f | |||
| bf2e3af4eb | |||
| d071a8a190 | |||
| afef38e45e | |||
| ac30d86b6a | |||
| 01cffb947c | |||
| fac5078dbf | |||
| 68ebe76f24 | |||
| c284cc191e | |||
| 90da7f8a21 | |||
| 2b94d5148f | |||
| 2574b9375d | |||
| e368bad7e4 | |||
| 5b6b3682dd | |||
| 92724c6ab0 | |||
| c9ddff46dd | |||
| 5ff8ddca92 | |||
| 85cfdca14c | |||
| 5482c8a52d | |||
| 2a6bbb82a6 | |||
| 24e4ac9284 | |||
| 81d70f18b3 | |||
| 85c9d03b99 | |||
| a147c495af | |||
| 4cf0224d2d | |||
| 7503dc8517 | |||
| 1c04318ecf | |||
| 4b577517a9 | |||
| c52478708f | |||
| 1d7ac19886 | |||
| 0686532be0 | |||
| 911074ae95 | |||
| 555f2ab9bd | |||
| a79077af17 | |||
| 61a4172bd4 | |||
| 589f94e5b2 | |||
| 8254ebbb47 | |||
| ea79fdae46 | |||
| 2013314e4f | |||
| 869c14e2b0 | |||
| cea079a770 | |||
| 9a7e5c7903 | |||
| 279d6a100f | |||
| 7afb72dca8 | |||
| c8cbc5dc96 | |||
| 6fba34d2e4 | |||
| 375b8b2df1 | |||
| b8776d7169 | |||
| 7a1631efa3 | |||
| fe7e0f397f | |||
| 839ca1fa0d | |||
| d51d324977 | |||
| 6f2c39aaf2 | |||
| 2a4a9d1290 | |||
| 29fcd67bf8 | |||
| faa8ac7c13 | |||
| 40e4c12cdd | |||
| 53c68250e4 | |||
| 2749983267 | |||
| 2c93f4a1ed | |||
| 51acdf1d31 | |||
| d21680ed4f | |||
| ca9732ecb2 | |||
| 4132793219 | |||
| d9ce3eeb5c | |||
| 24528ecfeb | |||
| a77a00c70a | |||
| 117f7be9c8 | |||
| bcb45077fb | |||
| 6c39acd54e | |||
| 518aadb9fd | |||
| 0dca331950 | |||
| 4c0fde7621 | |||
| 755fffda0f | |||
| af4cd25f47 | |||
| 59a500b879 | |||
| 71b5acee30 | |||
| 031e4ee054 | |||
| b6df63c79e | |||
| 6e8eeb984f | |||
| ffc9179cff | |||
| ef491340f6 | |||
| 7f65fda790 | |||
| 5918a03456 | |||
| 0337cc8a22 | |||
| 4d7245d8b0 | |||
| 81ac623fca | |||
| bdd5a41106 | |||
| 60457389a3 | |||
| 8f098e4da1 | |||
| a734cbeaec | |||
| 8f167e7de1 | |||
| 2c1a9be38e | |||
| 01b2ca02ff | |||
| 9d47d06064 | |||
| e7d1c875fd | |||
| d63ac73903 | |||
| 26315e2016 | |||
| 9f85625b83 | |||
| e17c5ad3b0 | |||
| 5f5f0fcf16 | |||
| bacb70e327 | |||
| 0ea5fae69d | |||
| 214872dfbf | |||
| 2b64fb7c7e | |||
| 438757ab54 | |||
| 67f15f12ca | |||
| 992d0e063c | |||
| 428434d98d | |||
| 20b07ddd7d | |||
| c094e78bef | |||
| 57345e8230 | |||
| d5d67102ac | |||
| 30a4071ee0 | |||
| 942ba84ca5 | |||
| ab3c821e59 | |||
| 1953e92b7b | |||
| 9cc625409f | |||
| 558d92fe23 | |||
| 0aef8f9491 | |||
| e275f4632d | |||
| 5166916926 | |||
| 56859dbe32 | |||
| 1272e427a1 | |||
| e396c895dc | |||
| f7d0889bbc | |||
| 9b84f3a1b9 | |||
| b1520fcca0 | |||
| bb51d91b76 | |||
| 2c43feec3e | |||
| 825de1d886 | |||
| 7397a2d69f | |||
| 7951ba67e3 | |||
| 2fcacb4313 | |||
| 5e480cd69b | |||
| bfbbac8952 | |||
| bb04864436 | |||
| b87e1b4a02 | |||
| 185ed49fe2 | |||
| a30face86a | |||
| 188376b75a | |||
| 6a98d9b0bd | |||
| a562a8e2ed | |||
| 215c4046d1 | |||
| d30c93989c | |||
| 4554f92fb4 | |||
| 556ed9b848 | |||
| 344e7e90c2 | |||
| c64bf16c8b | |||
| ce5ef3b77d | |||
| 8f47ee3679 | |||
| f15f229a09 | |||
| da12cdddd4 | |||
| 0bb47812e5 | |||
| 203e17ce87 | |||
| 32a417d6d1 | |||
| 2de7127f02 | |||
| a5a9688599 | |||
| ad49fc89d3 | |||
| 6329a8a0da | |||
| 69b793cbb8 | |||
| f8f01a0987 | |||
| 4ce52a7af6 | |||
| ca0471799b | |||
| 0febd3d644 | |||
| c085730ca5 | |||
| 538edb907d | |||
| a854a4ec02 | |||
| ec098c6d39 | |||
| 17bac07a86 | |||
| 63da6bb1bd | |||
| e847ecd54c | |||
| 51ee4a3f54 | |||
| 8c4156948e | |||
| 89f8bb32d0 | |||
| e0a7eb8972 | |||
| 12da5a4704 | |||
| 749b5e3b9c | |||
| e74e2226d5 | |||
| 5a1534e51e | |||
| 22451300b1 | |||
| a50e59ab1d | |||
| 6d86eab021 | |||
| 6b306eecf2 | |||
| 90857fae0a | |||
| b37ebb81f1 | |||
| e37b8e1265 | |||
| 3ef4029c4a | |||
| cb189e3de4 | |||
| 4fee8e1d07 | |||
| 64298f09c1 | |||
| 60bfa28ef0 | |||
| 8c5a2ef611 | |||
| db1ffb5918 | |||
| fe39a75e6e | |||
| 1d33b29c27 | |||
| 831edfc8c6 | |||
| c3ab279d7d | |||
| 6820b63d56 | |||
| fdc099c960 | |||
| afb531da59 | |||
| 7c867f1788 | |||
| a3415095a7 | |||
| 3e3bebf3e0 | |||
| 99a21324db | |||
| 92d5801f63 | |||
| 551421818b | |||
| de1373deae | |||
| f9f9706900 | |||
| 74105f0fde | |||
| 72c667d790 | |||
| 82ed5b3e0b | |||
| b489abba6b | |||
| 203a20592a | |||
| 7059d3a8d0 | |||
| 289448d196 | |||
| c557656040 | |||
| 7189998699 | |||
| 00c9042c15 | |||
| d3c986e1f2 | |||
| 2f61bd8e57 | |||
| 61ad4119cf | |||
| ab31006d01 | |||
| 584a38c7a7 | |||
| 1d817c7f50 | |||
| b327bb074e | |||
| 8ed2b0df7f | |||
| a21ac83f21 | |||
| 13e15cc7c4 | |||
| ad6e168303 | |||
| 73627b1b2d | |||
| 1219b42046 | |||
| ba2470aefe | |||
| 8543c3ab6d | |||
| 9a16ceefbb | |||
| 549595c1c6 | |||
| 8125ea2e3b | |||
| 3e4270729b | |||
| 9aa156eb2a | |||
| 4e3ae3b378 | |||
| 339f85c27b | |||
| aff7fa309c | |||
| cb02041b3b | |||
| e8ecc037c9 | |||
| 652531ac63 | |||
| a8ffd36150 | |||
| 143a021067 | |||
| 487e1a38aa | |||
| e70d637ade | |||
| 8fa4ab24fb | |||
| 14810708f0 | |||
| 3e907d8e43 | |||
| bf457d94ce | |||
| e045ca5a49 | |||
| 84a50111c0 | |||
| 3fae717912 | |||
| 995dd96117 | |||
| c911e79d88 | |||
| f507c63069 | |||
| 723d2c8354 | |||
| 9ab813e74d | |||
| 69576f7e5b | |||
| 3a66c90f34 | |||
| 5481db63df | |||
| 5f2697540a | |||
| 8e4b3d15a4 | |||
| cbe135ed0d | |||
| 73dd006bb2 | |||
| fea09ce93c | |||
| 9fa588fef8 | |||
| f45a5fd0a7 | |||
| 25001fd3e4 | |||
| 3d98ec35e7 | |||
| 5bbe8959f4 | |||
| 3bb001c374 | |||
| c32926cc34 | |||
| 54fb92a780 | |||
| 27d6740dc5 | |||
| cab7c605dc | |||
| e77baffa10 | |||
| 179c4eaff5 | |||
| 32b6da5aca | |||
| 564a2fb9b2 | |||
| 25cc643f34 | |||
| daa0a9b4e6 | |||
| 97931c3766 | |||
| f796a85617 | |||
| 50120e0f9b | |||
| 8715a6171a | |||
| baa77c68e0 | |||
| b176d2ff6f | |||
| 7db0e8da1d | |||
| 754fc91376 | |||
| fb9d8f3b9a | |||
| cad8877b7a | |||
| eae82128ee | |||
| 9f188f6313 | |||
| 6b1afc975e | |||
| 49e883827e | |||
| 2900a40b38 | |||
| a59b687b62 | |||
| 8ba4b4d2c1 | |||
| eeaf061932 | |||
| c3101aa995 | |||
| ec3ec7a26f | |||
| 19b7a0c00c | |||
| 9e6eac557f | |||
| 1623994287 | |||
| fbc6da1a8f | |||
| 49981f2f3e | |||
| 4009fe0899 | |||
| dd8783c8c1 | |||
| 9562b8a280 | |||
| e83856baa9 | |||
| bb690e12c2 | |||
| 41237dccfd | |||
| 6067adc120 | |||
| 275dde61ae | |||
| ab36e6b613 | |||
| a131c761e5 | |||
| 380c34aa66 | |||
| 7f40cfdab0 | |||
| 6db480b91d | |||
| 02e2a20aff | |||
| 1bcedcee0f | |||
| ad405d130e | |||
| c83526a6a4 | |||
| 9bd19aa042 | |||
| 2f62d82420 | |||
| 7f809fd875 | |||
| 8495bf6ad8 | |||
| 9d41971d3f | |||
| c409d500b3 | |||
| d0ab59669f | |||
| d264e39285 | |||
| 5b34f3b55b | |||
| 192ac3f843 | |||
| 581d4e32d0 | |||
| bb87d69224 | |||
| 0714d1f77b | |||
| 78af8c3219 | |||
| fd27429713 | |||
| a270e368c8 | |||
| a368fd5d7f | |||
| 0f982887a3 | |||
| beebc5fa58 | |||
| f8bc0679ee | |||
| cdca7984ed | |||
| 816fc48077 | |||
| 144483be4c | |||
| eb4e77f8c5 | |||
| 6ddbdac9cd | |||
| ecb9a1cfaf | |||
| 61fc93abf1 | |||
| 7ce28e6d0f | |||
| 1a4936d250 | |||
| 9692557746 | |||
| 8df6bfd19c | |||
| 5e66e9799c | |||
| 5ecb0e7986 | |||
| c3a5161db1 | |||
| 57aebe97ec | |||
| 651174dc57 | |||
| 4b6301cb37 | |||
| 1db80e1e13 | |||
| 938f4fec13 | |||
| daa505152a | |||
| 02d4ea9ca7 | |||
| 3633905459 | |||
| 2265a5dc67 | |||
| 616d9cdce2 | |||
| 213deb6e99 | |||
| 7ff443898c | |||
| d7c1674c6c | |||
| 4f39ac7dfe | |||
| 8d24badcf2 | |||
| 6c7f7130cf | |||
| d8b6130428 | |||
| dae573e318 | |||
| 674e4b0b14 | |||
| 828b55c747 | |||
| 596d64280b | |||
| 1683d875a0 | |||
| ed4658ccd0 | |||
| bc39df0ee5 | |||
| 70695d8c92 | |||
| 0533e2db3a | |||
| 716149c06c | |||
| 0628dd528a | |||
| e146ad6e7d | |||
| cf2c4fd179 | |||
| 6ba61ef80b | |||
| e45f990487 | |||
| 8e6c912a07 | |||
| 9a411f2403 | |||
| d2c65c91b7 | |||
| 5078aec840 | |||
| df949ab8ee | |||
| 2e036783bf | |||
| ff145300b4 | |||
| c2de9d8e83 | |||
| baeda3773d | |||
| 725f1845d9 | |||
| e0949a0eaa | |||
| 5d2bcd8366 | |||
| 5181d0e403 | |||
| 1c31fa80d2 | |||
| d3d08276ae | |||
| 80b34280c2 | |||
| 0cfb7625bf | |||
| 4c3653e009 | |||
| 9512a5dd2f | |||
| 4f5e3f7162 | |||
| 89c2ff58f8 | |||
| 0813ce3ffb | |||
| a030554ab6 | |||
| e1d36fc0c7 | |||
| 0dca8e0eff | |||
| 4cf90d83a3 | |||
| 9b4462f880 | |||
| 87b7b2642d | |||
| 5eee5c7c63 | |||
| c6dc1d1474 | |||
| 8c86918970 | |||
| 200d5d1f41 | |||
| 3499d4aa74 | |||
| 7dbd66b467 | |||
| db8b0bf142 | |||
| 6e54471e57 | |||
| 3f383c1a88 | |||
| 4577657c90 | |||
| 94662b6e75 |
Generated
+53
-10
@@ -37,16 +37,6 @@ version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
||||
|
||||
[[package]]
|
||||
name = "bt-demo"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"fidc-core",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.2"
|
||||
@@ -109,12 +99,43 @@ version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb"
|
||||
dependencies = [
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
@@ -125,8 +146,10 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
name = "fidc-core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"chrono",
|
||||
"indexmap",
|
||||
"rayon",
|
||||
"rhai",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -304,6 +327,26 @@ version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rhai"
|
||||
version = "1.23.6"
|
||||
|
||||
+2
-1
@@ -1,7 +1,6 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"crates/fidc-core",
|
||||
"crates/bt-demo",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
@@ -12,9 +11,11 @@ version = "0.1.0"
|
||||
authors = ["OpenAI Codex"]
|
||||
|
||||
[workspace.dependencies]
|
||||
ahash = "=0.8.12"
|
||||
chrono = { version = "=0.4.44", features = ["serde"] }
|
||||
indexmap = { version = "=2.11.4", features = ["serde"] }
|
||||
reqwest = { version = "=0.12.24", default-features = false, features = ["json", "rustls-tls"] }
|
||||
rayon = "=1.12.0"
|
||||
rhai = { version = "=1.23.6", features = ["sync"] }
|
||||
serde = { version = "=1.0.228", features = ["derive"] }
|
||||
serde_json = "=1.0.145"
|
||||
|
||||
@@ -2,9 +2,19 @@
|
||||
|
||||
面向中国 A 股和期货策略的 Rust 回测核心。仓库目标是提供平台自有的策略 DSL、执行模型、撮合模型和结果分析能力,最终由 `fidc-backtest-service` 对外提供策略运行服务。
|
||||
|
||||
## Runtime position exposure schedule
|
||||
|
||||
`strategy_spec.runtimeExpressions.risk.positionExposureSchedule` accepts dated
|
||||
`effectiveDate` plus `targetExposureBps` points. The platform expression strategy
|
||||
uses the latest point whose date is not later than the current execution date and
|
||||
otherwise keeps the strategy's normal `exposureExpr`. This contract is intended for
|
||||
audited runtime controls replayed by paper/live shadow reconciliation; it is not a
|
||||
market-data signal and does not change selection, pricing, fees, or execution-day
|
||||
risk checks.
|
||||
|
||||
## 当前能力
|
||||
|
||||
- 日频、分钟、tick 级策略生命周期与确定性回放。
|
||||
- 日频和分钟执行价策略生命周期与确定性回放。
|
||||
- A 股行情、估值、因子、基准、候选资格、涨跌停触达、停牌和 ST 标记。
|
||||
- 平台策略 DSL 与 `StrategyContext` 数据 API,不暴露非平台脚本语法。
|
||||
- `BacktestConfig` 支持起止日期、初始资金、决策滞后、执行价格字段、基准代码。
|
||||
@@ -14,6 +24,7 @@
|
||||
- Broker 支持目标权重、显式金额、目标股数、限价、VWAP/TWAP、挂单、撤单和订单查询。
|
||||
- 期货账户支持多空持仓、开平仓、今昨仓、保证金、手续费、结算和到期处理。
|
||||
- 报告输出支持权益曲线、成交、持仓、月度收益、风险指标、基准序列和 JSON 分析包。
|
||||
- 账户出入金以独立外部现金流记录保存;权益曲线同时输出 `externalCashFlow` 和现金流中性 `unitNav`,收益指标不把入金/出金计入交易收益。延迟出金在结算前做整批资金校验,不能把账户现金变成负数。
|
||||
- 内置 `OmniMicroCapStrategy`,覆盖动态市值带、均线过滤、止损止盈、固定频率再平衡和盘中执行近似。
|
||||
|
||||
## Workspace 布局
|
||||
@@ -22,7 +33,6 @@
|
||||
.
|
||||
├── Cargo.toml
|
||||
├── crates
|
||||
│ ├── bt-demo
|
||||
│ └── fidc-core
|
||||
│ └── src
|
||||
│ ├── broker.rs
|
||||
@@ -37,7 +47,6 @@
|
||||
│ ├── scheduler.rs
|
||||
│ ├── strategy.rs
|
||||
│ └── strategy_ai.rs
|
||||
├── data/demo
|
||||
└── docs
|
||||
```
|
||||
|
||||
@@ -51,7 +60,7 @@
|
||||
- `futures`: 期货账户、合约参数、保证金、手续费和多空持仓。
|
||||
- `rules`: 中国市场交易规则和风控校验。
|
||||
- `broker`: 股票撮合、订单簿、滑点、成交量约束、限价和显式订单执行。
|
||||
- `scheduler`: 日、周、月、分钟、tick 调度规则。
|
||||
- `scheduler`: 日、周、月和分钟调度规则。
|
||||
- `platform_expr_strategy`: 平台 DSL 解析后的表达式策略执行模型。
|
||||
- `strategy`: 策略 trait、内置策略和运行时视图。
|
||||
- `strategy_ai`: 策略 AI 手册、提示词生成和数据库字段目录合并。
|
||||
@@ -76,12 +85,22 @@
|
||||
- `selection.market_cap_band(...)` 动态市值带。
|
||||
- `filter.stock_expr(...)` 任意指标、因子和组合选股。
|
||||
- `ordering.rank_by(...)` 与 `ordering.rank_expr(...)` 排序。
|
||||
- `allocation.buy_scale(...)` 动态买入资金比例。
|
||||
- `allocation.buy_scale(...)` 相对等权槽位的个股资金倍率;显式权重可以大于 `1.0`,组合总仓位仍由 `risk.index_exposure(...)` 和严格资金预算控制。
|
||||
- `risk.stop_loss(...)`、`risk.take_profit(...)` 多条件止盈止损。
|
||||
- `order.*`、`cancel.*`、`update_universe(...)`、`subscribe(...)` 显式交易动作。
|
||||
|
||||
任意数据库指标和自定义因子通过 `factor("field")`、`factor_value("field", lookback)`、`rolling_mean("field", n)`、`sma("close", n)` 等函数读取。未预计算的均线窗口可在回测中按已有历史数据实时计算。
|
||||
|
||||
Source Lake 日线成交量保留原始可用性合同:源 `volume=null` 与真实 `volume=0` 含义不同。依赖成交量的 rolling 窗口只要包含源空值就返回缺失,不得把空值补成 0;停牌日明确提供的 0 成交量仍是合法观测。该合同随 runner 快照版本冻结,旧快照不能跨版本复用。
|
||||
|
||||
盘后固定价格不是策略类型,也不是 `matchingType`。自 2026-07-06 起,只有实际同日提交时间落在 15:00–15:30 的普通委托才由 broker 进入盘后固定价格执行阶段;15:00–15:04 的委托等待到 15:05,15:05–15:30 按官方收盘价和真实盘后成交量撮合,不叠加滑点,未成交余量不跨日。窗口外委托继续沿用连续竞价、当前收盘或下一交易日开盘合同;`next_bar_open` 策略即使在 15:00 生成信号,也不得被改写为同日盘后委托。缺失盘后行情时必须明确不成交,禁止回退全天成交量或 15:00 前分钟行情。
|
||||
|
||||
分钟回放使用行情时间戳与策略定时事件的有序合并时钟。`OnDay`、`Bar` 或 `Minute` 阶段只要声明显式 `physical_time`,就必须在各自真实分钟进入同一时间轴,即使该分钟没有预加载行情也必须触发,并由执行层按需查询该时点或之前最新有效价格;同一时间戳只形成一个事件,scheduler 回调先于 `on_minute`。日线 `current_bar_close` 无显式时间时使用官方日收盘,有显式时间时使用该触发点的 `Last` 行情,禁止读取下一分钟;多个时间点分别执行,禁止压成最后一个时间、把早盘单改成盘后单或依赖已有 BAR 才触发。`next_bar_open` 的 T 日信号时钟继续留在粗粒度决策阶段,不能延迟到 T+1 的同名分钟。
|
||||
|
||||
`holdUntilExit=true` 与 `stopTakeReferencePriceMode=signal_day_post_adjusted_close` 组合表示持久模型组合语义:股票进入模型目标后即记录信号日和后复权参考价,不以买单是否成交为前提。涨停、停牌或其他执行风控导致买单未成交时,模型成员仍占用目标槽位、每天累计模型持有日并继续生成目标仓位;达到止盈、止损或最大模型持有期后才从模型组合移除。实际订单仍由成交日风控独立决定,不得用实际持仓集合覆盖模型目标集合。
|
||||
|
||||
`targetPortfolioDaily=true` 时,每只股票的默认目标金额固定为 `target_budget / selection_limit * buy_scale`,候选不足、缺行情或风控拒绝产生的剩余资金保留为现金,不得自动归一到满仓。止盈、止损或最大持有期触发后,标的从活动目标顺序移除;卖出未完成时继续占用仓位槽且不得反向补买,成功释放的槽位只能由同一决策时点已排序且通过策略条件的后续候选补充。只有显式设置 `redistributeTargetWeightsAfterExit=true` 才在可用目标间重新分配权重,只有显式设置 `reenterExitedTargets=true` 才保留退出标的供后续重新入场;两个开关互相独立,默认都为 `false`。
|
||||
|
||||
## 内置微盘策略
|
||||
|
||||
`OmniMicroCapStrategy` 是平台内置的微盘轮动策略,用于 demo、性能验证和策略迁移基线:
|
||||
@@ -96,45 +115,9 @@
|
||||
|
||||
## 运行方式
|
||||
|
||||
默认运行仓库 demo 数据:
|
||||
`fidc-backtest-engine` 不再维护本地 CSV demo、partitioned snapshot 目录或导出融合表作为运行入口。生产和集成回测由 `fidc-backtest-service` runner 创建 `DataSet`,数据来自 Strategy Factory Source Lake 的 Arrow/Parquet、manifest/data_epoch 缓存和运行时逻辑视图。
|
||||
|
||||
```bash
|
||||
cargo run --bin bt-demo
|
||||
```
|
||||
|
||||
运行平台内置微盘策略:
|
||||
|
||||
```bash
|
||||
FIDC_BT_STRATEGY=omni-microcap \
|
||||
FIDC_BT_SIGNAL_SYMBOL=000001.SH \
|
||||
cargo run --release --bin bt-demo
|
||||
```
|
||||
|
||||
接入真实分区 snapshot 目录:
|
||||
|
||||
```bash
|
||||
FIDC_BT_DATA_LAYOUT=partitioned \
|
||||
FIDC_BT_DATA_DIR=/path/to/snapshots \
|
||||
FIDC_BT_SIGNAL_SYMBOL=000001.SH \
|
||||
cargo run --bin bt-demo
|
||||
```
|
||||
|
||||
约定目录结构:
|
||||
|
||||
```text
|
||||
snapshots/
|
||||
├── instruments.csv
|
||||
├── benchmark/YYYY/MM/*.csv
|
||||
├── market/YYYY/MM/*.csv
|
||||
├── factors/YYYY/MM/*.csv
|
||||
└── candidates/YYYY/MM/*.csv
|
||||
```
|
||||
|
||||
运行后默认生成:
|
||||
|
||||
- `output/demo/equity_curve.csv`
|
||||
- `output/demo/trades.csv`
|
||||
- `output/demo/holdings_summary.csv`
|
||||
本仓库只保留核心库构建和测试入口:
|
||||
|
||||
## 测试与构建
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
[package]
|
||||
name = "bt-demo"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
chrono = { workspace = true }
|
||||
fidc-core = { path = "../fidc-core" }
|
||||
serde = { workspace = true }
|
||||
serde_json = "1"
|
||||
@@ -1,567 +0,0 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::error::Error;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use chrono::{NaiveDate, NaiveTime};
|
||||
use fidc_core::{
|
||||
BacktestConfig, BacktestEngine, BenchmarkSnapshot, BrokerSimulator, ChinaAShareCostModel,
|
||||
ChinaEquityRuleHooks, CnSmallCapRotationConfig, CnSmallCapRotationStrategy, DailyEquityPoint,
|
||||
DataSet, FillEvent, HoldingSummary, OmniMicroCapConfig, OmniMicroCapStrategy, PortfolioState,
|
||||
PriceField, Strategy, StrategyContext,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let root = workspace_root();
|
||||
let data_dir = std::env::var("FIDC_BT_DATA_DIR")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| root.join("data/demo"));
|
||||
let data_layout = std::env::var("FIDC_BT_DATA_LAYOUT").unwrap_or_else(|_| "flat".to_string());
|
||||
let output_dir = std::env::var("FIDC_BT_OUTPUT_DIR")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| root.join("output/demo"));
|
||||
let json_output = std::env::var("FIDC_BT_JSON")
|
||||
.map(|value| value == "1" || value.eq_ignore_ascii_case("true"))
|
||||
.unwrap_or(false);
|
||||
|
||||
fs::create_dir_all(&output_dir)?;
|
||||
|
||||
let data = if data_layout == "partitioned" {
|
||||
DataSet::from_partitioned_dir(&data_dir)?
|
||||
} else {
|
||||
DataSet::from_csv_dir(&data_dir)?
|
||||
};
|
||||
let strategy_name =
|
||||
std::env::var("FIDC_BT_STRATEGY").unwrap_or_else(|_| "cn-smallcap-rotation".to_string());
|
||||
let debug_date = std::env::var("FIDC_BT_DEBUG_DATE")
|
||||
.ok()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.map(|value| NaiveDate::parse_from_str(value.trim(), "%Y-%m-%d"))
|
||||
.transpose()?;
|
||||
let decision_lag = std::env::var("FIDC_BT_DECISION_LAG")
|
||||
.ok()
|
||||
.and_then(|value| value.parse::<usize>().ok());
|
||||
let execution_price =
|
||||
std::env::var("FIDC_BT_EXECUTION_PRICE")
|
||||
.ok()
|
||||
.map(|value| match value.as_str() {
|
||||
"close" => PriceField::Close,
|
||||
"last" => PriceField::Last,
|
||||
_ => PriceField::Open,
|
||||
});
|
||||
let initial_cash = std::env::var("FIDC_BT_INITIAL_CASH")
|
||||
.ok()
|
||||
.and_then(|value| value.parse::<f64>().ok());
|
||||
let start_date = std::env::var("FIDC_BT_START_DATE")
|
||||
.ok()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.map(|value| NaiveDate::parse_from_str(value.trim(), "%Y-%m-%d"))
|
||||
.transpose()?;
|
||||
let end_date = std::env::var("FIDC_BT_END_DATE")
|
||||
.ok()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.map(|value| NaiveDate::parse_from_str(value.trim(), "%Y-%m-%d"))
|
||||
.transpose()?;
|
||||
let mut config = BacktestConfig {
|
||||
initial_cash: initial_cash.unwrap_or(1_000_000.0),
|
||||
benchmark_code: data.benchmark_code().to_string(),
|
||||
start_date,
|
||||
end_date,
|
||||
decision_lag_trading_days: 1,
|
||||
execution_price_field: PriceField::Open,
|
||||
};
|
||||
let result = match strategy_name.as_str() {
|
||||
"cn-smallcap-rotation" | "cn-dyn-smallcap-band" => {
|
||||
let mut strategy_cfg = if strategy_name == "cn-dyn-smallcap-band" {
|
||||
CnSmallCapRotationConfig::cn_dyn_smallcap_band()
|
||||
} else {
|
||||
CnSmallCapRotationConfig::demo()
|
||||
};
|
||||
if strategy_cfg.strategy_name == "cn-smallcap-rotation" {
|
||||
strategy_cfg.base_index_level = 3000.0;
|
||||
strategy_cfg.base_cap_floor = 38.0;
|
||||
strategy_cfg.cap_span = 25.0;
|
||||
}
|
||||
if let Ok(signal_symbol) = std::env::var("FIDC_BT_SIGNAL_SYMBOL") {
|
||||
if !signal_symbol.trim().is_empty() {
|
||||
strategy_cfg.signal_symbol = Some(signal_symbol);
|
||||
}
|
||||
}
|
||||
config.decision_lag_trading_days = decision_lag.unwrap_or(1);
|
||||
config.execution_price_field = execution_price.unwrap_or(PriceField::Open);
|
||||
let strategy = CnSmallCapRotationStrategy::new(strategy_cfg);
|
||||
let broker = BrokerSimulator::new_with_execution_price(
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
config.execution_price_field,
|
||||
);
|
||||
let mut engine = BacktestEngine::new(data, strategy, broker, config);
|
||||
engine.run()?
|
||||
}
|
||||
"aiquant-v104" => {
|
||||
let mut strategy_cfg = OmniMicroCapConfig::aiquant_v104();
|
||||
if let Ok(signal_symbol) = std::env::var("FIDC_BT_SIGNAL_SYMBOL") {
|
||||
if !signal_symbol.trim().is_empty() {
|
||||
strategy_cfg.benchmark_signal_symbol = signal_symbol;
|
||||
}
|
||||
}
|
||||
if let Some(date) = debug_date {
|
||||
let eligible = data.eligible_universe_on(date);
|
||||
eprintln!(
|
||||
"DEBUG eligible_universe_on {} count={}",
|
||||
date,
|
||||
eligible.len()
|
||||
);
|
||||
for row in eligible.iter().take(20) {
|
||||
eprintln!(" {} {:.6}", row.symbol, row.market_cap_bn);
|
||||
}
|
||||
let mut debug_strategy = OmniMicroCapStrategy::new(strategy_cfg.clone());
|
||||
let debug_subscriptions = BTreeSet::new();
|
||||
let decision = debug_strategy.on_day(&StrategyContext {
|
||||
execution_date: date,
|
||||
decision_date: date,
|
||||
decision_index: 1,
|
||||
data: &data,
|
||||
portfolio: &PortfolioState::new(20_000.0),
|
||||
futures_account: None,
|
||||
open_orders: &[],
|
||||
dynamic_universe: None,
|
||||
subscriptions: &debug_subscriptions,
|
||||
process_events: &[],
|
||||
active_process_event: None,
|
||||
active_datetime: None,
|
||||
order_events: &[],
|
||||
fills: &[],
|
||||
})?;
|
||||
eprintln!("DEBUG notes={:?}", decision.notes);
|
||||
eprintln!("DEBUG diagnostics={:?}", decision.diagnostics);
|
||||
return Ok(());
|
||||
}
|
||||
config.decision_lag_trading_days = decision_lag.unwrap_or(1);
|
||||
config.execution_price_field = execution_price.unwrap_or(PriceField::Close);
|
||||
config.initial_cash = initial_cash.unwrap_or(20_000.0);
|
||||
let strategy = OmniMicroCapStrategy::new(strategy_cfg);
|
||||
let broker = BrokerSimulator::new_with_execution_price(
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
config.execution_price_field,
|
||||
);
|
||||
let mut engine = BacktestEngine::new(data, strategy, broker, config);
|
||||
engine.run()?
|
||||
}
|
||||
_ => {
|
||||
let mut strategy_cfg = OmniMicroCapConfig::omni_microcap();
|
||||
if let Ok(signal_symbol) = std::env::var("FIDC_BT_SIGNAL_SYMBOL") {
|
||||
if !signal_symbol.trim().is_empty() {
|
||||
strategy_cfg.benchmark_signal_symbol = signal_symbol;
|
||||
}
|
||||
}
|
||||
if let Some(date) = debug_date {
|
||||
let eligible = data.eligible_universe_on(date);
|
||||
eprintln!(
|
||||
"DEBUG eligible_universe_on {} count={}",
|
||||
date,
|
||||
eligible.len()
|
||||
);
|
||||
for row in eligible.iter().take(20) {
|
||||
eprintln!(" {} {:.6}", row.symbol, row.market_cap_bn);
|
||||
}
|
||||
let mut debug_strategy = OmniMicroCapStrategy::new(strategy_cfg.clone());
|
||||
let debug_subscriptions = BTreeSet::new();
|
||||
let decision = debug_strategy.on_day(&StrategyContext {
|
||||
execution_date: date,
|
||||
decision_date: date,
|
||||
decision_index: 1,
|
||||
data: &data,
|
||||
portfolio: &PortfolioState::new(10_000_000.0),
|
||||
futures_account: None,
|
||||
open_orders: &[],
|
||||
dynamic_universe: None,
|
||||
subscriptions: &debug_subscriptions,
|
||||
process_events: &[],
|
||||
active_process_event: None,
|
||||
active_datetime: None,
|
||||
order_events: &[],
|
||||
fills: &[],
|
||||
})?;
|
||||
eprintln!("DEBUG notes={:?}", decision.notes);
|
||||
eprintln!("DEBUG diagnostics={:?}", decision.diagnostics);
|
||||
return Ok(());
|
||||
}
|
||||
config.decision_lag_trading_days = decision_lag.unwrap_or(0);
|
||||
config.execution_price_field = execution_price.unwrap_or(PriceField::Last);
|
||||
config.initial_cash = initial_cash.unwrap_or(10_000_000.0);
|
||||
let strategy = OmniMicroCapStrategy::new(strategy_cfg);
|
||||
let broker = BrokerSimulator::new_with_execution_price(
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
config.execution_price_field,
|
||||
)
|
||||
.with_intraday_execution_start_time(
|
||||
NaiveTime::parse_from_str("10:18:00", "%H:%M:%S").expect("valid 10:18:00"),
|
||||
)
|
||||
.with_volume_limit(false)
|
||||
.with_inactive_limit(false)
|
||||
.with_liquidity_limit(false);
|
||||
let mut engine = BacktestEngine::new(data, strategy, broker, config);
|
||||
engine.run()?
|
||||
}
|
||||
};
|
||||
|
||||
write_equity_curve_csv(&output_dir.join("equity_curve.csv"), &result.equity_curve)?;
|
||||
write_trades_csv(&output_dir.join("trades.csv"), &result.fills)?;
|
||||
write_holdings_csv(
|
||||
&output_dir.join("holdings_summary.csv"),
|
||||
&result.holdings_summary,
|
||||
)?;
|
||||
|
||||
let summary = build_summary(
|
||||
&result.strategy_name,
|
||||
&result.equity_curve,
|
||||
&result.fills,
|
||||
&result.holdings_summary,
|
||||
result.benchmark_series.last(),
|
||||
&output_dir,
|
||||
);
|
||||
|
||||
print_summary(&summary, &result.equity_curve, &result.holdings_summary);
|
||||
println!("Artifacts written under {}", output_dir.display());
|
||||
if json_output {
|
||||
println!("{}", serde_json::to_string(&summary)?);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn workspace_root() -> PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../..")
|
||||
.canonicalize()
|
||||
.expect("workspace root")
|
||||
}
|
||||
|
||||
fn write_equity_curve_csv(path: &Path, rows: &[DailyEquityPoint]) -> Result<(), Box<dyn Error>> {
|
||||
let mut file = fs::File::create(path)?;
|
||||
writeln!(
|
||||
file,
|
||||
"date,cash,market_value,total_equity,benchmark_close,benchmark_prev_close,notes,diagnostics"
|
||||
)?;
|
||||
for row in rows {
|
||||
writeln!(
|
||||
file,
|
||||
"{},{:.2},{:.2},{:.2},{:.2},{:.2},{},{}",
|
||||
row.date,
|
||||
row.cash,
|
||||
row.market_value,
|
||||
row.total_equity,
|
||||
row.benchmark_close,
|
||||
row.benchmark_prev_close,
|
||||
sanitize_csv_field(&row.notes),
|
||||
sanitize_csv_field(&row.diagnostics),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_trades_csv(path: &Path, rows: &[FillEvent]) -> Result<(), Box<dyn Error>> {
|
||||
let mut file = fs::File::create(path)?;
|
||||
writeln!(
|
||||
file,
|
||||
"date,symbol,side,quantity,price,gross_amount,commission,stamp_tax,net_cash_flow,reason"
|
||||
)?;
|
||||
for row in rows {
|
||||
writeln!(
|
||||
file,
|
||||
"{},{},{:?},{},{:.2},{:.2},{:.2},{:.2},{:.2},{}",
|
||||
row.date,
|
||||
row.symbol,
|
||||
row.side,
|
||||
row.quantity,
|
||||
row.price,
|
||||
row.gross_amount,
|
||||
row.commission,
|
||||
row.stamp_tax,
|
||||
row.net_cash_flow,
|
||||
sanitize_csv_field(&row.reason),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_holdings_csv(path: &Path, rows: &[HoldingSummary]) -> Result<(), Box<dyn Error>> {
|
||||
let mut file = fs::File::create(path)?;
|
||||
writeln!(
|
||||
file,
|
||||
"date,symbol,quantity,average_cost,last_price,market_value,unrealized_pnl,realized_pnl"
|
||||
)?;
|
||||
for row in rows {
|
||||
writeln!(
|
||||
file,
|
||||
"{},{},{},{:.2},{:.2},{:.2},{:.2},{:.2}",
|
||||
row.date,
|
||||
row.symbol,
|
||||
row.quantity,
|
||||
row.average_cost,
|
||||
row.last_price,
|
||||
row.market_value,
|
||||
row.unrealized_pnl,
|
||||
row.realized_pnl,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sanitize_csv_field(text: &str) -> String {
|
||||
text.replace(',', ";")
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
struct RunSummary {
|
||||
strategy: String,
|
||||
start_date: String,
|
||||
end_date: String,
|
||||
start_equity: f64,
|
||||
final_equity: f64,
|
||||
total_return: f64,
|
||||
trade_count: usize,
|
||||
holding_count: usize,
|
||||
benchmark_code: Option<String>,
|
||||
benchmark_last_close: Option<f64>,
|
||||
output_dir: String,
|
||||
diagnostics: serde_json::Value,
|
||||
warnings: Vec<String>,
|
||||
equity_preview: Vec<serde_json::Value>,
|
||||
trades_preview: Vec<serde_json::Value>,
|
||||
}
|
||||
|
||||
fn build_summary(
|
||||
strategy_name: &str,
|
||||
equity_curve: &[DailyEquityPoint],
|
||||
fills: &[FillEvent],
|
||||
holdings: &[HoldingSummary],
|
||||
benchmark_last: Option<&BenchmarkSnapshot>,
|
||||
output_dir: &Path,
|
||||
) -> RunSummary {
|
||||
let first = equity_curve.first();
|
||||
let last = equity_curve.last();
|
||||
let start_equity = first.map(|row| row.total_equity).unwrap_or_default();
|
||||
let final_equity = last.map(|row| row.total_equity).unwrap_or_default();
|
||||
let total_return = if start_equity.abs() < f64::EPSILON {
|
||||
0.0
|
||||
} else {
|
||||
(final_equity / start_equity) - 1.0
|
||||
};
|
||||
|
||||
let diagnostics = extract_diagnostics(equity_curve);
|
||||
let warnings = build_warnings(fills, holdings, &diagnostics);
|
||||
let equity_preview = equity_curve
|
||||
.iter()
|
||||
.rev()
|
||||
.take(5)
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.rev()
|
||||
.map(|row| {
|
||||
json!({
|
||||
"date": row.date.to_string(),
|
||||
"cash": row.cash,
|
||||
"marketValue": row.market_value,
|
||||
"totalEquity": row.total_equity,
|
||||
"benchmarkClose": row.benchmark_close,
|
||||
"benchmarkPrevClose": row.benchmark_prev_close,
|
||||
"notes": row.notes,
|
||||
"diagnostics": row.diagnostics,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let trades_preview = fills
|
||||
.iter()
|
||||
.rev()
|
||||
.take(10)
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.rev()
|
||||
.map(|row| {
|
||||
json!({
|
||||
"date": row.date.to_string(),
|
||||
"symbol": row.symbol,
|
||||
"side": format!("{:?}", row.side),
|
||||
"quantity": row.quantity,
|
||||
"price": row.price,
|
||||
"grossAmount": row.gross_amount,
|
||||
"netCashFlow": row.net_cash_flow,
|
||||
"reason": row.reason,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
RunSummary {
|
||||
strategy: strategy_name.to_string(),
|
||||
start_date: first.map(|row| row.date.to_string()).unwrap_or_default(),
|
||||
end_date: last.map(|row| row.date.to_string()).unwrap_or_default(),
|
||||
start_equity,
|
||||
final_equity,
|
||||
total_return,
|
||||
trade_count: fills.len(),
|
||||
holding_count: holdings.len(),
|
||||
benchmark_code: benchmark_last.map(|row| row.benchmark.clone()),
|
||||
benchmark_last_close: benchmark_last.map(|row| row.close),
|
||||
output_dir: output_dir.display().to_string(),
|
||||
diagnostics,
|
||||
warnings,
|
||||
equity_preview,
|
||||
trades_preview,
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_diagnostics(equity_curve: &[DailyEquityPoint]) -> serde_json::Value {
|
||||
let last = equity_curve.last();
|
||||
let text = last.map(|row| row.diagnostics.as_str()).unwrap_or("");
|
||||
let notes = last.map(|row| row.notes.as_str()).unwrap_or("");
|
||||
let mut map = serde_json::Map::new();
|
||||
map.insert("latestText".to_string(), json!(text));
|
||||
map.insert("latestNotes".to_string(), json!(notes));
|
||||
map.insert("equityPointCount".to_string(), json!(equity_curve.len()));
|
||||
|
||||
for part in text.split(" | ") {
|
||||
let part = part.trim();
|
||||
if let Some(rest) = part.strip_prefix("selection_diag ") {
|
||||
for token in rest.split_whitespace() {
|
||||
if let Some((k, v)) = token.split_once('=') {
|
||||
map.insert(k.to_string(), parse_diag_value(v));
|
||||
}
|
||||
}
|
||||
} else if let Some(rest) = part.strip_prefix("selection_band ") {
|
||||
for token in rest.split_whitespace() {
|
||||
if let Some((k, v)) = token.split_once('=') {
|
||||
map.insert(k.to_string(), parse_diag_value(v));
|
||||
}
|
||||
}
|
||||
} else if let Some(rest) =
|
||||
part.strip_prefix("market_cap_missing likely blocks selection; sample=")
|
||||
{
|
||||
map.insert(
|
||||
"marketCapMissingSample".to_string(),
|
||||
json!(
|
||||
rest.split('|')
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
),
|
||||
);
|
||||
} else if let Some(rest) = part.strip_prefix("selection_rejections sample=") {
|
||||
map.insert(
|
||||
"selectionRejectionsSample".to_string(),
|
||||
json!(
|
||||
rest.split(" | ")
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
),
|
||||
);
|
||||
} else if let Some(rest) = part.strip_prefix("ma_filter_rejections sample=") {
|
||||
map.insert(
|
||||
"maFilterRejectionsSample".to_string(),
|
||||
json!(
|
||||
rest.split('|')
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
),
|
||||
);
|
||||
} else if let Some(rest) = part.strip_prefix("selected=") {
|
||||
map.insert("selectedLine".to_string(), json!(rest));
|
||||
}
|
||||
}
|
||||
|
||||
serde_json::Value::Object(map)
|
||||
}
|
||||
|
||||
fn parse_diag_value(value: &str) -> serde_json::Value {
|
||||
if let Ok(v) = value.parse::<i64>() {
|
||||
return json!(v);
|
||||
}
|
||||
if let Ok(v) = value.parse::<f64>() {
|
||||
return json!(v);
|
||||
}
|
||||
json!(value)
|
||||
}
|
||||
|
||||
fn build_warnings(
|
||||
fills: &[FillEvent],
|
||||
holdings: &[HoldingSummary],
|
||||
diagnostics: &serde_json::Value,
|
||||
) -> Vec<String> {
|
||||
let mut warnings = Vec::new();
|
||||
if fills.is_empty() {
|
||||
warnings.push("本次回测没有产生任何成交。".to_string());
|
||||
}
|
||||
if holdings.is_empty() {
|
||||
warnings.push("期末没有持仓。".to_string());
|
||||
}
|
||||
let selected_after_ma_is_empty = diagnostics
|
||||
.get("selected_after_ma")
|
||||
.and_then(|v| v.as_i64())
|
||||
.unwrap_or(0)
|
||||
== 0;
|
||||
if selected_after_ma_is_empty && fills.is_empty() && holdings.is_empty() {
|
||||
warnings
|
||||
.push("最终没有股票通过完整选股链路,结果为空时请优先查看 diagnostics。".to_string());
|
||||
}
|
||||
if diagnostics
|
||||
.get("market_cap_missing_count")
|
||||
.and_then(|v| v.as_i64())
|
||||
.unwrap_or(0)
|
||||
> 0
|
||||
{
|
||||
warnings.push("存在 market_cap 缺失或非正值,当前会直接阻断该股票进入候选池。".to_string());
|
||||
}
|
||||
warnings
|
||||
}
|
||||
|
||||
fn print_summary(
|
||||
summary: &RunSummary,
|
||||
equity_curve: &[DailyEquityPoint],
|
||||
holdings: &[HoldingSummary],
|
||||
) {
|
||||
if equity_curve.is_empty() {
|
||||
println!("No equity curve points generated.");
|
||||
return;
|
||||
}
|
||||
|
||||
println!("Strategy: {}", summary.strategy);
|
||||
println!("Start equity: {:.2}", summary.start_equity);
|
||||
println!("Final equity: {:.2}", summary.final_equity);
|
||||
println!("Total return: {:.2}%", summary.total_return * 100.0);
|
||||
println!("Trades: {}", summary.trade_count);
|
||||
println!("Final holdings: {}", summary.holding_count);
|
||||
|
||||
if let (Some(code), Some(close)) = (&summary.benchmark_code, summary.benchmark_last_close) {
|
||||
println!("Benchmark last close: {} {:.2}", code, close);
|
||||
}
|
||||
|
||||
println!("Recent equity points:");
|
||||
for point in equity_curve
|
||||
.iter()
|
||||
.rev()
|
||||
.take(3)
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.rev()
|
||||
{
|
||||
println!(
|
||||
" {} equity {:.2} cash {:.2} mv {:.2}",
|
||||
point.date, point.total_equity, point.cash, point.market_value
|
||||
);
|
||||
}
|
||||
|
||||
if holdings.is_empty() {
|
||||
println!("No holdings at the end of the demo run.");
|
||||
} else {
|
||||
println!("Ending holdings:");
|
||||
for holding in holdings {
|
||||
println!(
|
||||
" {} qty {} mv {:.2} pnl {:.2}",
|
||||
holding.symbol, holding.quantity, holding.market_value, holding.unrealized_pnl
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,10 @@ license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
ahash.workspace = true
|
||||
chrono.workspace = true
|
||||
indexmap.workspace = true
|
||||
rayon.workspace = true
|
||||
rhai.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
+7782
-493
File diff suppressed because it is too large
Load Diff
+217
-59
@@ -3,18 +3,32 @@ use std::collections::BTreeMap;
|
||||
use chrono::NaiveDate;
|
||||
|
||||
use crate::events::OrderSide;
|
||||
|
||||
pub const STOCK_PIT_TAX_CHANGE_DATE: (i32, u32, u32) = (2023, 8, 28);
|
||||
use crate::fixed_point::{FixedChinaAShareCostModel, FixedMoney, FixedTradingCost};
|
||||
use crate::risk_control::TradingConstraintConfig;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct TradingCost {
|
||||
pub commission: f64,
|
||||
pub stamp_tax: f64,
|
||||
pub transfer_fee: f64,
|
||||
}
|
||||
|
||||
impl TradingCost {
|
||||
pub fn total(self) -> f64 {
|
||||
self.commission + self.stamp_tax
|
||||
self.fixed_total().to_f64()
|
||||
}
|
||||
|
||||
pub fn fixed_total(self) -> FixedMoney {
|
||||
FixedMoney::checked_sum_f64([self.commission, self.stamp_tax, self.transfer_fee])
|
||||
.expect("trading costs must be finite fixed-point money")
|
||||
}
|
||||
|
||||
fn from_fixed(value: FixedTradingCost) -> Self {
|
||||
Self {
|
||||
commission: value.commission.to_f64(),
|
||||
stamp_tax: value.stamp_tax.to_f64(),
|
||||
transfer_fee: value.transfer_fee.to_f64(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,50 +49,133 @@ pub trait CostModel {
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ChinaAShareCostModel {
|
||||
pub commission_rate: f64,
|
||||
pub stamp_tax_rate_before_change: f64,
|
||||
pub stamp_tax_rate_after_change: f64,
|
||||
pub minimum_commission: f64,
|
||||
fixed: FixedChinaAShareCostModel,
|
||||
}
|
||||
|
||||
impl Default for ChinaAShareCostModel {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
commission_rate: 0.0003,
|
||||
stamp_tax_rate_before_change: 0.001,
|
||||
stamp_tax_rate_after_change: 0.0005,
|
||||
minimum_commission: 5.0,
|
||||
}
|
||||
Self::from_trading_constraints(TradingConstraintConfig::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl ChinaAShareCostModel {
|
||||
pub fn from_trading_constraints(config: TradingConstraintConfig) -> Self {
|
||||
Self {
|
||||
fixed: FixedChinaAShareCostModel {
|
||||
commission_rate: Self::fixed_money(config.commission_rate, "commission rate"),
|
||||
stamp_tax_rate_before_change: Self::fixed_money(
|
||||
config.stamp_tax_rate_before_change,
|
||||
"stamp tax rate before change",
|
||||
),
|
||||
stamp_tax_rate_after_change: Self::fixed_money(
|
||||
config.stamp_tax_rate_after_change,
|
||||
"stamp tax rate after change",
|
||||
),
|
||||
stamp_tax_change_date: config.stamp_tax_change_date,
|
||||
minimum_commission: Self::fixed_money(
|
||||
config.minimum_commission,
|
||||
"minimum commission",
|
||||
),
|
||||
transfer_fee_rate: Self::fixed_money(config.transfer_fee_rate, "transfer fee rate"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_commission_rate(&mut self, value: f64) {
|
||||
self.fixed.commission_rate = Self::fixed_money(value, "commission rate");
|
||||
}
|
||||
|
||||
pub fn set_minimum_commission(&mut self, value: f64) {
|
||||
self.fixed.minimum_commission = Self::fixed_money(value, "minimum commission");
|
||||
}
|
||||
|
||||
pub fn set_transfer_fee_rate(&mut self, value: f64) {
|
||||
self.fixed.transfer_fee_rate = Self::fixed_money(value, "transfer fee rate");
|
||||
}
|
||||
|
||||
pub fn set_stamp_tax_rate_before_change(&mut self, value: f64) {
|
||||
self.fixed.stamp_tax_rate_before_change =
|
||||
Self::fixed_money(value, "stamp tax rate before change");
|
||||
}
|
||||
|
||||
pub fn set_stamp_tax_rate_after_change(&mut self, value: f64) {
|
||||
self.fixed.stamp_tax_rate_after_change =
|
||||
Self::fixed_money(value, "stamp tax rate after change");
|
||||
}
|
||||
|
||||
pub fn set_stamp_tax_change_date(&mut self, value: NaiveDate) {
|
||||
self.fixed.stamp_tax_change_date = value;
|
||||
}
|
||||
|
||||
pub fn commission_rate(&self) -> f64 {
|
||||
self.fixed.commission_rate.to_f64()
|
||||
}
|
||||
|
||||
pub fn minimum_commission(&self) -> f64 {
|
||||
self.fixed.minimum_commission.to_f64()
|
||||
}
|
||||
|
||||
pub fn transfer_fee_rate(&self) -> f64 {
|
||||
self.fixed.transfer_fee_rate.to_f64()
|
||||
}
|
||||
|
||||
pub fn stamp_tax_rate_before_change(&self) -> f64 {
|
||||
self.fixed.stamp_tax_rate_before_change.to_f64()
|
||||
}
|
||||
|
||||
pub fn stamp_tax_rate_after_change(&self) -> f64 {
|
||||
self.fixed.stamp_tax_rate_after_change.to_f64()
|
||||
}
|
||||
|
||||
pub fn stamp_tax_change_date(&self) -> NaiveDate {
|
||||
self.fixed.stamp_tax_change_date
|
||||
}
|
||||
|
||||
pub fn with_commission_rate(mut self, value: f64) -> Self {
|
||||
self.set_commission_rate(value);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_minimum_commission(mut self, value: f64) -> Self {
|
||||
self.set_minimum_commission(value);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_stamp_tax_rates(mut self, before: f64, after: f64) -> Self {
|
||||
self.set_stamp_tax_rate_before_change(before);
|
||||
self.set_stamp_tax_rate_after_change(after);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn commission_for(&self, gross_amount: f64) -> f64 {
|
||||
if gross_amount <= 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
(gross_amount * self.commission_rate).max(self.minimum_commission)
|
||||
self.fixed_model()
|
||||
.commission_for(Self::fixed_money(gross_amount, "gross amount"))
|
||||
.to_f64()
|
||||
}
|
||||
|
||||
pub fn stamp_tax_rate_for(&self, date: NaiveDate) -> f64 {
|
||||
let change_date = NaiveDate::from_ymd_opt(
|
||||
STOCK_PIT_TAX_CHANGE_DATE.0,
|
||||
STOCK_PIT_TAX_CHANGE_DATE.1,
|
||||
STOCK_PIT_TAX_CHANGE_DATE.2,
|
||||
)
|
||||
.expect("valid pit tax change date");
|
||||
if date < change_date {
|
||||
self.stamp_tax_rate_before_change
|
||||
} else {
|
||||
self.stamp_tax_rate_after_change
|
||||
}
|
||||
self.fixed.stamp_tax_rate_for(date).to_f64()
|
||||
}
|
||||
|
||||
pub fn stamp_tax_for(&self, date: NaiveDate, side: OrderSide, gross_amount: f64) -> f64 {
|
||||
if gross_amount <= 0.0 || side == OrderSide::Buy {
|
||||
return 0.0;
|
||||
}
|
||||
gross_amount * self.stamp_tax_rate_for(date)
|
||||
self.fixed_model()
|
||||
.stamp_tax_for(date, side, Self::fixed_money(gross_amount, "gross amount"))
|
||||
.to_f64()
|
||||
}
|
||||
|
||||
pub fn transfer_fee_for(&self, gross_amount: f64) -> f64 {
|
||||
if gross_amount <= 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
self.fixed_model()
|
||||
.transfer_fee_for(Self::fixed_money(gross_amount, "gross amount"))
|
||||
.to_f64()
|
||||
}
|
||||
|
||||
pub fn commission_for_order_fill(
|
||||
@@ -91,31 +188,29 @@ impl ChinaAShareCostModel {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let raw_commission = gross_amount * self.commission_rate;
|
||||
let Some(order_id) = order_id else {
|
||||
return raw_commission.max(self.minimum_commission);
|
||||
return self.commission_for(gross_amount);
|
||||
};
|
||||
|
||||
let remaining_minimum = commission_state
|
||||
.entry(order_id)
|
||||
.or_insert(self.minimum_commission);
|
||||
if raw_commission > *remaining_minimum {
|
||||
let charged = if (*remaining_minimum - self.minimum_commission).abs() < 1e-12 {
|
||||
raw_commission
|
||||
} else {
|
||||
raw_commission - *remaining_minimum
|
||||
};
|
||||
*remaining_minimum = 0.0;
|
||||
charged
|
||||
} else {
|
||||
let charged = if (*remaining_minimum - self.minimum_commission).abs() < 1e-12 {
|
||||
self.minimum_commission
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
*remaining_minimum -= raw_commission;
|
||||
charged
|
||||
}
|
||||
.or_insert(self.fixed.minimum_commission.to_f64());
|
||||
let mut fixed_remaining = Self::fixed_money(*remaining_minimum, "remaining commission");
|
||||
let charged = self.fixed_model().commission_for_order_fill_remaining(
|
||||
Self::fixed_money(gross_amount, "gross amount"),
|
||||
&mut fixed_remaining,
|
||||
);
|
||||
*remaining_minimum = fixed_remaining.to_f64();
|
||||
charged.to_f64()
|
||||
}
|
||||
|
||||
fn fixed_money(value: f64, label: &str) -> FixedMoney {
|
||||
FixedMoney::from_f64(value)
|
||||
.unwrap_or_else(|| panic!("{label} is not representable as fixed-point money: {value}"))
|
||||
}
|
||||
|
||||
fn fixed_model(&self) -> FixedChinaAShareCostModel {
|
||||
self.fixed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,16 +220,15 @@ impl CostModel for ChinaAShareCostModel {
|
||||
return TradingCost {
|
||||
commission: 0.0,
|
||||
stamp_tax: 0.0,
|
||||
transfer_fee: 0.0,
|
||||
};
|
||||
}
|
||||
|
||||
let commission = self.commission_for(gross_amount);
|
||||
let stamp_tax = self.stamp_tax_for(date, side, gross_amount);
|
||||
|
||||
TradingCost {
|
||||
commission,
|
||||
stamp_tax,
|
||||
}
|
||||
TradingCost::from_fixed(self.fixed_model().calculate(
|
||||
date,
|
||||
side,
|
||||
Self::fixed_money(gross_amount, "gross amount"),
|
||||
))
|
||||
}
|
||||
|
||||
fn calculate_with_order_state(
|
||||
@@ -149,15 +243,79 @@ impl CostModel for ChinaAShareCostModel {
|
||||
return TradingCost {
|
||||
commission: 0.0,
|
||||
stamp_tax: 0.0,
|
||||
transfer_fee: 0.0,
|
||||
};
|
||||
}
|
||||
|
||||
let commission = self.commission_for_order_fill(gross_amount, order_id, commission_state);
|
||||
let stamp_tax = self.stamp_tax_for(date, side, gross_amount);
|
||||
|
||||
TradingCost {
|
||||
let fixed_model = self.fixed_model();
|
||||
let fixed_gross = Self::fixed_money(gross_amount, "gross amount");
|
||||
let commission = if let Some(order_id) = order_id {
|
||||
let remaining = commission_state
|
||||
.entry(order_id)
|
||||
.or_insert(self.fixed.minimum_commission.to_f64());
|
||||
let mut fixed_remaining = Self::fixed_money(*remaining, "remaining commission");
|
||||
let commission =
|
||||
fixed_model.commission_for_order_fill_remaining(fixed_gross, &mut fixed_remaining);
|
||||
*remaining = fixed_remaining.to_f64();
|
||||
commission
|
||||
} else {
|
||||
fixed_model.commission_for(fixed_gross)
|
||||
};
|
||||
TradingCost::from_fixed(FixedTradingCost {
|
||||
commission,
|
||||
stamp_tax,
|
||||
}
|
||||
stamp_tax: fixed_model.stamp_tax_for(date, side, fixed_gross),
|
||||
transfer_fee: fixed_model.transfer_fee_for(fixed_gross),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_quantizes_fees_to_micro_yuan() {
|
||||
let model = ChinaAShareCostModel::default();
|
||||
let date = NaiveDate::from_ymd_opt(2025, 11, 11).expect("valid date");
|
||||
|
||||
assert!((model.commission_for(248_059.812) - 74.417944).abs() < 1e-12);
|
||||
assert!(
|
||||
(model.stamp_tax_for(date, OrderSide::Sell, 245_747.007) - 122.873504).abs() < 1e-12
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_model_can_use_configurable_stamp_tax_change_date() {
|
||||
let config = TradingConstraintConfig {
|
||||
commission_rate: 0.0003,
|
||||
minimum_commission: 5.0,
|
||||
transfer_fee_rate: 0.00001,
|
||||
stamp_tax_rate_before_change: 0.002,
|
||||
stamp_tax_rate_after_change: 0.001,
|
||||
stamp_tax_change_date: NaiveDate::from_ymd_opt(2025, 1, 10).expect("valid date"),
|
||||
..TradingConstraintConfig::default()
|
||||
};
|
||||
let model = ChinaAShareCostModel::from_trading_constraints(config);
|
||||
|
||||
assert!((model.transfer_fee_for(10_000.0) - 0.1).abs() < 1e-12);
|
||||
|
||||
assert!(
|
||||
(model.stamp_tax_for(
|
||||
NaiveDate::from_ymd_opt(2025, 1, 9).expect("valid date"),
|
||||
OrderSide::Sell,
|
||||
10_000.0
|
||||
) - 20.0)
|
||||
.abs()
|
||||
< 1e-9
|
||||
);
|
||||
assert!(
|
||||
(model.stamp_tax_for(
|
||||
NaiveDate::from_ymd_opt(2025, 1, 10).expect("valid date"),
|
||||
OrderSide::Sell,
|
||||
10_000.0
|
||||
) - 10.0)
|
||||
.abs()
|
||||
< 1e-9
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+4887
-1002
File diff suppressed because it is too large
Load Diff
+3869
-393
File diff suppressed because it is too large
Load Diff
@@ -125,6 +125,15 @@ impl ProcessEventBus {
|
||||
loader.install_enabled(self, enabled_names)
|
||||
}
|
||||
|
||||
pub fn has_listeners_for(&self, kinds: &[ProcessEventKind]) -> bool {
|
||||
!self.any_listeners.is_empty()
|
||||
|| kinds.iter().any(|kind| {
|
||||
self.listeners
|
||||
.get(kind)
|
||||
.is_some_and(|listeners| !listeners.is_empty())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn publish(&mut self, event: &ProcessEvent) {
|
||||
if let Some(listeners) = self.listeners.get_mut(&event.kind) {
|
||||
for listener in listeners {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use chrono::NaiveDate;
|
||||
use chrono::{NaiveDate, NaiveDateTime};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
mod date_format {
|
||||
@@ -23,6 +23,62 @@ mod date_format {
|
||||
}
|
||||
}
|
||||
|
||||
mod optional_date_format {
|
||||
use chrono::NaiveDate;
|
||||
use serde::{self, Deserialize, Deserializer, Serializer};
|
||||
|
||||
const FORMAT: &str = "%Y-%m-%d";
|
||||
|
||||
pub fn serialize<S>(date: &Option<NaiveDate>, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match date {
|
||||
Some(date) => serializer.serialize_some(&date.format(FORMAT).to_string()),
|
||||
None => serializer.serialize_none(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<NaiveDate>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let value = Option::<String>::deserialize(deserializer)?;
|
||||
value
|
||||
.map(|text| NaiveDate::parse_from_str(&text, FORMAT).map_err(serde::de::Error::custom))
|
||||
.transpose()
|
||||
}
|
||||
}
|
||||
|
||||
mod optional_datetime_format {
|
||||
use chrono::NaiveDateTime;
|
||||
use serde::{self, Deserialize, Deserializer, Serializer};
|
||||
|
||||
const FORMAT: &str = "%Y-%m-%d %H:%M:%S%.f";
|
||||
|
||||
pub fn serialize<S>(datetime: &Option<NaiveDateTime>, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match datetime {
|
||||
Some(datetime) => serializer.serialize_some(&datetime.format(FORMAT).to_string()),
|
||||
None => serializer.serialize_none(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<NaiveDateTime>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let value = Option::<String>::deserialize(deserializer)?;
|
||||
value
|
||||
.map(|text| {
|
||||
NaiveDateTime::parse_from_str(&text, FORMAT).map_err(serde::de::Error::custom)
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum OrderSide {
|
||||
Buy,
|
||||
@@ -45,6 +101,7 @@ pub enum OrderStatus {
|
||||
PartiallyFilled,
|
||||
Canceled,
|
||||
Rejected,
|
||||
Expired,
|
||||
}
|
||||
|
||||
impl OrderStatus {
|
||||
@@ -55,6 +112,7 @@ impl OrderStatus {
|
||||
Self::PartiallyFilled => "partially_filled",
|
||||
Self::Canceled => "canceled",
|
||||
Self::Rejected => "rejected",
|
||||
Self::Expired => "expired",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,6 +121,12 @@ impl OrderStatus {
|
||||
pub struct OrderEvent {
|
||||
#[serde(with = "date_format")]
|
||||
pub date: NaiveDate,
|
||||
#[serde(default, with = "optional_date_format")]
|
||||
pub decision_date: Option<NaiveDate>,
|
||||
#[serde(default, with = "optional_date_format")]
|
||||
pub order_created_date: Option<NaiveDate>,
|
||||
#[serde(default, with = "optional_date_format")]
|
||||
pub execution_date: Option<NaiveDate>,
|
||||
#[serde(default)]
|
||||
pub order_id: Option<u64>,
|
||||
pub symbol: String,
|
||||
@@ -73,10 +137,72 @@ pub struct OrderEvent {
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
impl OrderEvent {
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
if self.symbol.trim().is_empty() || self.requested_quantity == 0 {
|
||||
return Err(format!(
|
||||
"invalid order identity/quantity order_id={:?} symbol={} requested={}",
|
||||
self.order_id, self.symbol, self.requested_quantity
|
||||
));
|
||||
}
|
||||
if self.filled_quantity > self.requested_quantity {
|
||||
return Err(format!(
|
||||
"order overfill order_id={:?} requested={} filled={}",
|
||||
self.order_id, self.requested_quantity, self.filled_quantity
|
||||
));
|
||||
}
|
||||
let quantity_valid = match self.status {
|
||||
OrderStatus::Pending => self.filled_quantity < self.requested_quantity,
|
||||
OrderStatus::Filled => self.filled_quantity == self.requested_quantity,
|
||||
OrderStatus::PartiallyFilled => {
|
||||
self.filled_quantity > 0 && self.filled_quantity < self.requested_quantity
|
||||
}
|
||||
OrderStatus::Canceled => self.filled_quantity < self.requested_quantity,
|
||||
OrderStatus::Rejected => self.filled_quantity == 0,
|
||||
OrderStatus::Expired => self.filled_quantity < self.requested_quantity,
|
||||
};
|
||||
if !quantity_valid {
|
||||
return Err(format!(
|
||||
"order status/quantity mismatch order_id={:?} status={} requested={} filled={}",
|
||||
self.order_id,
|
||||
self.status.as_str(),
|
||||
self.requested_quantity,
|
||||
self.filled_quantity
|
||||
));
|
||||
}
|
||||
if self.reason.trim().is_empty() {
|
||||
return Err(format!(
|
||||
"order reason is empty order_id={:?} status={}",
|
||||
self.order_id,
|
||||
self.status.as_str()
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FillEvent {
|
||||
#[serde(with = "date_format")]
|
||||
pub date: NaiveDate,
|
||||
#[serde(default, with = "optional_date_format")]
|
||||
pub decision_date: Option<NaiveDate>,
|
||||
#[serde(default, with = "optional_date_format")]
|
||||
pub order_created_date: Option<NaiveDate>,
|
||||
#[serde(default, with = "optional_date_format")]
|
||||
pub execution_date: Option<NaiveDate>,
|
||||
#[serde(
|
||||
default,
|
||||
with = "optional_datetime_format",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub execution_start_timestamp: Option<NaiveDateTime>,
|
||||
#[serde(
|
||||
default,
|
||||
with = "optional_datetime_format",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub execution_timestamp: Option<NaiveDateTime>,
|
||||
#[serde(default)]
|
||||
pub order_id: Option<u64>,
|
||||
pub symbol: String,
|
||||
@@ -86,10 +212,47 @@ pub struct FillEvent {
|
||||
pub gross_amount: f64,
|
||||
pub commission: f64,
|
||||
pub stamp_tax: f64,
|
||||
pub transfer_fee: f64,
|
||||
pub net_cash_flow: f64,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
impl FillEvent {
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
if self.symbol.trim().is_empty()
|
||||
|| self.quantity == 0
|
||||
|| !self.price.is_finite()
|
||||
|| self.price <= 0.0
|
||||
{
|
||||
return Err(format!(
|
||||
"invalid fill identity/quantity/price order_id={:?} symbol={} quantity={} price={}",
|
||||
self.order_id, self.symbol, self.quantity, self.price
|
||||
));
|
||||
}
|
||||
if let (Some(start), Some(end)) = (self.execution_start_timestamp, self.execution_timestamp)
|
||||
{
|
||||
if start > end {
|
||||
return Err(format!(
|
||||
"fill execution timestamp order is invalid order_id={:?} start={} end={}",
|
||||
self.order_id, start, end
|
||||
));
|
||||
}
|
||||
if start.date() != self.date || end.date() != self.date {
|
||||
return Err(format!(
|
||||
"fill execution timestamp date mismatch order_id={:?} fill_date={} start={} end={}",
|
||||
self.order_id, self.date, start, end
|
||||
));
|
||||
}
|
||||
} else if self.execution_start_timestamp.is_some() || self.execution_timestamp.is_some() {
|
||||
return Err(format!(
|
||||
"fill execution timestamp range is incomplete order_id={:?}",
|
||||
self.order_id
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PositionEvent {
|
||||
#[serde(with = "date_format")]
|
||||
@@ -123,9 +286,9 @@ pub enum ProcessEventKind {
|
||||
PreBar,
|
||||
Bar,
|
||||
PostBar,
|
||||
PreTick,
|
||||
Tick,
|
||||
PostTick,
|
||||
PreMinute,
|
||||
Minute,
|
||||
PostMinute,
|
||||
PreScheduled,
|
||||
PostScheduled,
|
||||
PreOnDay,
|
||||
@@ -143,6 +306,9 @@ pub enum ProcessEventKind {
|
||||
OrderPendingCancel,
|
||||
OrderCancellationPass,
|
||||
OrderCancellationReject,
|
||||
OrderPendingUpdate,
|
||||
OrderUpdatePass,
|
||||
OrderUpdateReject,
|
||||
OrderUnsolicitedUpdate,
|
||||
Trade,
|
||||
UniverseUpdated,
|
||||
@@ -165,9 +331,9 @@ impl ProcessEventKind {
|
||||
Self::PreBar => "pre_bar",
|
||||
Self::Bar => "bar",
|
||||
Self::PostBar => "post_bar",
|
||||
Self::PreTick => "pre_tick",
|
||||
Self::Tick => "tick",
|
||||
Self::PostTick => "post_tick",
|
||||
Self::PreMinute => "pre_minute",
|
||||
Self::Minute => "minute",
|
||||
Self::PostMinute => "post_minute",
|
||||
Self::PreScheduled => "pre_scheduled",
|
||||
Self::PostScheduled => "post_scheduled",
|
||||
Self::PreOnDay => "pre_on_day",
|
||||
@@ -185,6 +351,9 @@ impl ProcessEventKind {
|
||||
Self::OrderPendingCancel => "order_pending_cancel",
|
||||
Self::OrderCancellationPass => "order_cancellation_pass",
|
||||
Self::OrderCancellationReject => "order_cancellation_reject",
|
||||
Self::OrderPendingUpdate => "order_pending_update",
|
||||
Self::OrderUpdatePass => "order_update_pass",
|
||||
Self::OrderUpdateReject => "order_update_reject",
|
||||
Self::OrderUnsolicitedUpdate => "order_unsolicited_update",
|
||||
Self::Trade => "trade",
|
||||
Self::UniverseUpdated => "universe_updated",
|
||||
@@ -195,6 +364,38 @@ impl ProcessEventKind {
|
||||
Self::AccountManagementFee => "account_management_fee",
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the event is part of the durable business lifecycle
|
||||
/// audit. Phase boundary events are useful during interactive debugging,
|
||||
/// but retaining every minute phase marker for a long run is unnecessary.
|
||||
pub fn is_business_lifecycle(&self) -> bool {
|
||||
matches!(
|
||||
*self,
|
||||
Self::PreScheduled
|
||||
| Self::PostScheduled
|
||||
| Self::PreOnDay
|
||||
| Self::OnDay
|
||||
| Self::PostOnDay
|
||||
| Self::OrderPendingNew
|
||||
| Self::OrderCreationPass
|
||||
| Self::OrderCreationReject
|
||||
| Self::OrderPendingCancel
|
||||
| Self::OrderCancellationPass
|
||||
| Self::OrderCancellationReject
|
||||
| Self::OrderPendingUpdate
|
||||
| Self::OrderUpdatePass
|
||||
| Self::OrderUpdateReject
|
||||
| Self::OrderUnsolicitedUpdate
|
||||
| Self::Trade
|
||||
| Self::UniverseUpdated
|
||||
| Self::UniverseSubscribed
|
||||
| Self::UniverseUnsubscribed
|
||||
| Self::AccountDepositWithdraw
|
||||
| Self::AccountFinanceRepay
|
||||
| Self::AccountManagementFee
|
||||
| Self::Settlement
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -210,3 +411,116 @@ pub struct ProcessEvent {
|
||||
pub side: Option<OrderSide>,
|
||||
pub detail: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::{NaiveDate, NaiveDateTime};
|
||||
|
||||
use super::{FillEvent, OrderEvent, OrderSide, OrderStatus, ProcessEventKind};
|
||||
|
||||
fn order_event(status: OrderStatus, filled_quantity: u32) -> OrderEvent {
|
||||
OrderEvent {
|
||||
date: NaiveDate::from_ymd_opt(2025, 1, 2).unwrap(),
|
||||
decision_date: None,
|
||||
order_created_date: None,
|
||||
execution_date: None,
|
||||
order_id: Some(1),
|
||||
symbol: "600000.SH".to_string(),
|
||||
side: OrderSide::Buy,
|
||||
requested_quantity: 100,
|
||||
filled_quantity,
|
||||
status,
|
||||
reason: "test".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn order_event_status_quantity_contract_is_explicit() {
|
||||
assert!(order_event(OrderStatus::Pending, 0).validate().is_ok());
|
||||
assert!(
|
||||
order_event(OrderStatus::PartiallyFilled, 40)
|
||||
.validate()
|
||||
.is_ok()
|
||||
);
|
||||
assert!(order_event(OrderStatus::Filled, 100).validate().is_ok());
|
||||
assert!(order_event(OrderStatus::Canceled, 40).validate().is_ok());
|
||||
assert!(order_event(OrderStatus::Rejected, 0).validate().is_ok());
|
||||
assert!(order_event(OrderStatus::Expired, 40).validate().is_ok());
|
||||
|
||||
assert!(
|
||||
order_event(OrderStatus::PartiallyFilled, 0)
|
||||
.validate()
|
||||
.is_err()
|
||||
);
|
||||
assert!(order_event(OrderStatus::Filled, 99).validate().is_err());
|
||||
assert!(order_event(OrderStatus::Canceled, 100).validate().is_err());
|
||||
assert!(order_event(OrderStatus::Rejected, 1).validate().is_err());
|
||||
assert!(order_event(OrderStatus::Expired, 100).validate().is_err());
|
||||
}
|
||||
|
||||
fn fill_event(start: Option<NaiveDateTime>, end: Option<NaiveDateTime>) -> FillEvent {
|
||||
FillEvent {
|
||||
date: NaiveDate::from_ymd_opt(2025, 1, 2).unwrap(),
|
||||
decision_date: None,
|
||||
order_created_date: None,
|
||||
execution_date: None,
|
||||
execution_start_timestamp: start,
|
||||
execution_timestamp: end,
|
||||
order_id: Some(1),
|
||||
symbol: "600000.SH".to_string(),
|
||||
side: OrderSide::Buy,
|
||||
quantity: 100,
|
||||
price: 10.0,
|
||||
gross_amount: 1_000.0,
|
||||
commission: 5.0,
|
||||
stamp_tax: 0.0,
|
||||
transfer_fee: 0.0,
|
||||
net_cash_flow: -1_005.0,
|
||||
reason: "test".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fill_execution_timestamp_range_is_explicit_and_backward_compatible() {
|
||||
let start = NaiveDate::from_ymd_opt(2025, 1, 2)
|
||||
.unwrap()
|
||||
.and_hms_opt(10, 18, 0)
|
||||
.unwrap();
|
||||
let end = start + chrono::Duration::seconds(3);
|
||||
assert!(fill_event(Some(start), Some(end)).validate().is_ok());
|
||||
assert!(fill_event(Some(end), Some(start)).validate().is_err());
|
||||
assert!(fill_event(Some(start), None).validate().is_err());
|
||||
|
||||
let next_day = start + chrono::Duration::days(1);
|
||||
assert!(
|
||||
fill_event(Some(next_day), Some(next_day))
|
||||
.validate()
|
||||
.is_err()
|
||||
);
|
||||
|
||||
let legacy = fill_event(None, None);
|
||||
let legacy_json = serde_json::to_value(&legacy).unwrap();
|
||||
assert!(legacy_json.get("execution_start_timestamp").is_none());
|
||||
assert!(legacy_json.get("execution_timestamp").is_none());
|
||||
let decoded: FillEvent = serde_json::from_value(legacy_json).unwrap();
|
||||
assert_eq!(decoded.execution_start_timestamp, None);
|
||||
assert_eq!(decoded.execution_timestamp, None);
|
||||
|
||||
let timestamped_json = serde_json::to_value(fill_event(Some(start), Some(end))).unwrap();
|
||||
assert_eq!(
|
||||
timestamped_json["execution_start_timestamp"],
|
||||
"2025-01-02 10:18:00"
|
||||
);
|
||||
assert_eq!(
|
||||
timestamped_json["execution_timestamp"],
|
||||
"2025-01-02 10:18:03"
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn process_event_business_lifecycle_filter_keeps_audit_events_only() {
|
||||
assert!(ProcessEventKind::OrderUpdateReject.is_business_lifecycle());
|
||||
assert!(ProcessEventKind::Settlement.is_business_lifecycle());
|
||||
assert!(!ProcessEventKind::PreMinute.is_business_lifecycle());
|
||||
assert!(!ProcessEventKind::PostBar.is_business_lifecycle());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,566 @@
|
||||
//! Fixed-point execution primitives for money and fee arithmetic.
|
||||
//!
|
||||
//! Market data and analytics remain floating point at their API boundaries.
|
||||
//! The execution kernel quantizes monetary values to micro-yuan before fee,
|
||||
//! budget and cash-ledger arithmetic so repeated fills and external cash flows
|
||||
//! do not accumulate binary floating-point drift.
|
||||
|
||||
use std::collections::{BTreeMap, VecDeque};
|
||||
|
||||
use chrono::NaiveDate;
|
||||
|
||||
use crate::events::OrderSide;
|
||||
|
||||
pub const MONEY_SCALE: i128 = 1_000_000;
|
||||
const MONEY_SCALE_F64: f64 = MONEY_SCALE as f64;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
||||
pub struct FixedMoney(i128);
|
||||
|
||||
impl FixedMoney {
|
||||
pub const ZERO: Self = Self(0);
|
||||
|
||||
pub const fn from_raw(raw: i128) -> Self {
|
||||
Self(raw)
|
||||
}
|
||||
|
||||
pub const fn raw(self) -> i128 {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn from_decimal_str(value: &str) -> Result<Self, String> {
|
||||
let value = value.trim();
|
||||
if value.is_empty() {
|
||||
return Err("fixed money value is empty".to_string());
|
||||
}
|
||||
let (negative, unsigned) = match value.as_bytes()[0] {
|
||||
b'-' => (true, &value[1..]),
|
||||
b'+' => (false, &value[1..]),
|
||||
_ => (false, value),
|
||||
};
|
||||
let mut parts = unsigned.split('.');
|
||||
let whole = parts.next().unwrap_or_default();
|
||||
let fractional = parts.next().unwrap_or_default();
|
||||
if parts.next().is_some()
|
||||
|| whole.is_empty()
|
||||
|| !whole.bytes().all(|byte| byte.is_ascii_digit())
|
||||
|| !fractional.bytes().all(|byte| byte.is_ascii_digit())
|
||||
{
|
||||
return Err(format!("invalid fixed money decimal: {value}"));
|
||||
}
|
||||
let whole = whole
|
||||
.parse::<i128>()
|
||||
.map_err(|_| format!("fixed money whole part is out of range: {value}"))?;
|
||||
let mut fractional_digits = fractional.as_bytes().to_vec();
|
||||
let round_up = fractional_digits.len() > 6 && fractional_digits[6] >= b'5';
|
||||
fractional_digits.truncate(6);
|
||||
while fractional_digits.len() < 6 {
|
||||
fractional_digits.push(b'0');
|
||||
}
|
||||
let fractional = if fractional_digits.is_empty() {
|
||||
0
|
||||
} else {
|
||||
std::str::from_utf8(&fractional_digits)
|
||||
.expect("fractional digits are ASCII")
|
||||
.parse::<i128>()
|
||||
.map_err(|_| format!("fixed money fractional part is invalid: {value}"))?
|
||||
};
|
||||
let mut raw = whole
|
||||
.checked_mul(MONEY_SCALE)
|
||||
.and_then(|raw| raw.checked_add(fractional))
|
||||
.ok_or_else(|| format!("fixed money value is out of range: {value}"))?;
|
||||
if round_up {
|
||||
raw = raw
|
||||
.checked_add(1)
|
||||
.ok_or_else(|| format!("fixed money value is out of range: {value}"))?;
|
||||
}
|
||||
Ok(Self(if negative { -raw } else { raw }))
|
||||
}
|
||||
|
||||
pub fn from_f64(value: f64) -> Option<Self> {
|
||||
if !value.is_finite() {
|
||||
return None;
|
||||
}
|
||||
let raw = (value * MONEY_SCALE_F64).round();
|
||||
if !raw.is_finite() || raw < i128::MIN as f64 || raw > i128::MAX as f64 {
|
||||
return None;
|
||||
}
|
||||
Some(Self(raw as i128))
|
||||
}
|
||||
|
||||
pub fn to_f64(self) -> f64 {
|
||||
self.0 as f64 / MONEY_SCALE_F64
|
||||
}
|
||||
|
||||
pub fn checked_add(self, other: Self) -> Option<Self> {
|
||||
self.0.checked_add(other.0).map(Self)
|
||||
}
|
||||
|
||||
pub fn checked_sub(self, other: Self) -> Option<Self> {
|
||||
self.0.checked_sub(other.0).map(Self)
|
||||
}
|
||||
|
||||
pub fn checked_mul_quantity(self, quantity: u64) -> Option<Self> {
|
||||
self.0.checked_mul(i128::from(quantity)).map(Self)
|
||||
}
|
||||
|
||||
pub fn checked_neg(self) -> Option<Self> {
|
||||
self.0.checked_neg().map(Self)
|
||||
}
|
||||
|
||||
pub fn checked_mul_rate(self, rate: Self) -> Option<Self> {
|
||||
let product = self.0.checked_mul(rate.0)?;
|
||||
let half = MONEY_SCALE / 2;
|
||||
let rounded = if product >= 0 {
|
||||
product.checked_add(half)? / MONEY_SCALE
|
||||
} else {
|
||||
product.checked_sub(half)? / MONEY_SCALE
|
||||
};
|
||||
Some(Self(rounded))
|
||||
}
|
||||
|
||||
pub fn checked_sum_f64(values: impl IntoIterator<Item = f64>) -> Option<Self> {
|
||||
values.into_iter().try_fold(Self::ZERO, |total, value| {
|
||||
total.checked_add(Self::from_f64(value)?)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn f64_fits_within(value: f64, limit: f64) -> Option<bool> {
|
||||
let value = Self::from_f64(value)?;
|
||||
if limit == f64::INFINITY {
|
||||
return Some(true);
|
||||
}
|
||||
Some(value <= Self::from_f64(limit)?)
|
||||
}
|
||||
|
||||
pub fn abs(self) -> Self {
|
||||
Self(self.0.abs())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct FixedTradingCost {
|
||||
pub commission: FixedMoney,
|
||||
pub stamp_tax: FixedMoney,
|
||||
pub transfer_fee: FixedMoney,
|
||||
}
|
||||
|
||||
impl FixedTradingCost {
|
||||
pub fn total(self) -> FixedMoney {
|
||||
FixedMoney::from_raw(self.commission.raw() + self.stamp_tax.raw() + self.transfer_fee.raw())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct FixedChinaAShareCostModel {
|
||||
pub commission_rate: FixedMoney,
|
||||
pub stamp_tax_rate_before_change: FixedMoney,
|
||||
pub stamp_tax_rate_after_change: FixedMoney,
|
||||
pub stamp_tax_change_date: NaiveDate,
|
||||
pub minimum_commission: FixedMoney,
|
||||
pub transfer_fee_rate: FixedMoney,
|
||||
}
|
||||
|
||||
impl FixedChinaAShareCostModel {
|
||||
pub fn commission_for(self, gross_amount: FixedMoney) -> FixedMoney {
|
||||
if gross_amount.raw() <= 0 {
|
||||
return FixedMoney::ZERO;
|
||||
}
|
||||
let raw = gross_amount
|
||||
.checked_mul_rate(self.commission_rate)
|
||||
.expect("fixed commission multiplication overflow");
|
||||
raw.max(self.minimum_commission)
|
||||
}
|
||||
|
||||
pub fn stamp_tax_rate_for(self, date: NaiveDate) -> FixedMoney {
|
||||
if date < self.stamp_tax_change_date {
|
||||
self.stamp_tax_rate_before_change
|
||||
} else {
|
||||
self.stamp_tax_rate_after_change
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stamp_tax_for(
|
||||
self,
|
||||
date: NaiveDate,
|
||||
side: OrderSide,
|
||||
gross_amount: FixedMoney,
|
||||
) -> FixedMoney {
|
||||
if gross_amount.raw() <= 0 || side == OrderSide::Buy {
|
||||
return FixedMoney::ZERO;
|
||||
}
|
||||
gross_amount
|
||||
.checked_mul_rate(self.stamp_tax_rate_for(date))
|
||||
.expect("fixed stamp tax multiplication overflow")
|
||||
}
|
||||
|
||||
pub fn transfer_fee_for(self, gross_amount: FixedMoney) -> FixedMoney {
|
||||
if gross_amount.raw() <= 0 {
|
||||
return FixedMoney::ZERO;
|
||||
}
|
||||
gross_amount
|
||||
.checked_mul_rate(self.transfer_fee_rate)
|
||||
.expect("fixed transfer fee multiplication overflow")
|
||||
}
|
||||
|
||||
pub fn calculate(
|
||||
self,
|
||||
date: NaiveDate,
|
||||
side: OrderSide,
|
||||
gross_amount: FixedMoney,
|
||||
) -> FixedTradingCost {
|
||||
FixedTradingCost {
|
||||
commission: self.commission_for(gross_amount),
|
||||
stamp_tax: self.stamp_tax_for(date, side, gross_amount),
|
||||
transfer_fee: self.transfer_fee_for(gross_amount),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn commission_for_order_fill(
|
||||
self,
|
||||
gross_amount: FixedMoney,
|
||||
order_id: Option<u64>,
|
||||
commission_state: &mut BTreeMap<u64, FixedMoney>,
|
||||
) -> FixedMoney {
|
||||
if gross_amount.raw() <= 0 {
|
||||
return FixedMoney::ZERO;
|
||||
}
|
||||
let raw = gross_amount
|
||||
.checked_mul_rate(self.commission_rate)
|
||||
.expect("fixed commission multiplication overflow");
|
||||
let Some(order_id) = order_id else {
|
||||
return raw.max(self.minimum_commission);
|
||||
};
|
||||
let remaining = commission_state
|
||||
.entry(order_id)
|
||||
.or_insert(self.minimum_commission);
|
||||
self.commission_for_order_fill_remaining(gross_amount, remaining)
|
||||
}
|
||||
|
||||
pub fn commission_for_order_fill_remaining(
|
||||
self,
|
||||
gross_amount: FixedMoney,
|
||||
remaining: &mut FixedMoney,
|
||||
) -> FixedMoney {
|
||||
if gross_amount.raw() <= 0 {
|
||||
return FixedMoney::ZERO;
|
||||
}
|
||||
let raw = gross_amount
|
||||
.checked_mul_rate(self.commission_rate)
|
||||
.expect("fixed commission multiplication overflow");
|
||||
if raw > *remaining {
|
||||
let charged = if *remaining == self.minimum_commission {
|
||||
raw
|
||||
} else {
|
||||
raw.checked_sub(*remaining)
|
||||
.expect("fixed remaining commission underflow")
|
||||
};
|
||||
*remaining = FixedMoney::ZERO;
|
||||
charged
|
||||
} else {
|
||||
let charged = if *remaining == self.minimum_commission {
|
||||
self.minimum_commission
|
||||
} else {
|
||||
FixedMoney::ZERO
|
||||
};
|
||||
*remaining = remaining
|
||||
.checked_sub(raw)
|
||||
.expect("fixed remaining commission underflow");
|
||||
charged
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct FixedLot {
|
||||
pub acquired_date: NaiveDate,
|
||||
pub quantity: u64,
|
||||
pub entry_price: FixedMoney,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct FixedLotBook {
|
||||
lots: VecDeque<FixedLot>,
|
||||
pub realized_pnl: FixedMoney,
|
||||
pub quantity: u64,
|
||||
}
|
||||
|
||||
impl FixedLotBook {
|
||||
pub fn buy(&mut self, date: NaiveDate, quantity: u64, price: FixedMoney) {
|
||||
if quantity == 0 {
|
||||
return;
|
||||
}
|
||||
self.lots.push_back(FixedLot {
|
||||
acquired_date: date,
|
||||
quantity,
|
||||
entry_price: price,
|
||||
});
|
||||
self.quantity = self.quantity.saturating_add(quantity);
|
||||
}
|
||||
|
||||
pub fn sell(&mut self, quantity: u64, price: FixedMoney) -> Result<FixedMoney, String> {
|
||||
if quantity > self.quantity {
|
||||
return Err(format!(
|
||||
"fixed sell quantity {} exceeds current quantity {}",
|
||||
quantity, self.quantity
|
||||
));
|
||||
}
|
||||
let mut remaining = quantity;
|
||||
let mut realized = FixedMoney::ZERO;
|
||||
while remaining > 0 {
|
||||
let Some(mut lot) = self.lots.pop_front() else {
|
||||
return Err("fixed lot book is empty while selling".to_string());
|
||||
};
|
||||
let sold = remaining.min(lot.quantity);
|
||||
let price_delta = price
|
||||
.checked_sub(lot.entry_price)
|
||||
.and_then(|delta| delta.checked_mul_quantity(sold))
|
||||
.ok_or_else(|| "fixed realized PnL overflow".to_string())?;
|
||||
realized = realized
|
||||
.checked_add(price_delta)
|
||||
.ok_or_else(|| "fixed realized PnL overflow".to_string())?;
|
||||
lot.quantity -= sold;
|
||||
remaining -= sold;
|
||||
if lot.quantity > 0 {
|
||||
self.lots.push_front(lot);
|
||||
}
|
||||
}
|
||||
self.quantity -= quantity;
|
||||
self.realized_pnl = self
|
||||
.realized_pnl
|
||||
.checked_add(realized)
|
||||
.ok_or_else(|| "fixed realized PnL overflow".to_string())?;
|
||||
Ok(realized)
|
||||
}
|
||||
|
||||
pub fn market_value(&self, mark_price: FixedMoney) -> FixedMoney {
|
||||
mark_price
|
||||
.checked_mul_quantity(self.quantity)
|
||||
.expect("fixed market value overflow")
|
||||
}
|
||||
|
||||
pub fn unrealized_pnl(&self, mark_price: FixedMoney) -> FixedMoney {
|
||||
self.lots.iter().fold(FixedMoney::ZERO, |total, lot| {
|
||||
let delta = mark_price
|
||||
.checked_sub(lot.entry_price)
|
||||
.and_then(|value| value.checked_mul_quantity(lot.quantity))
|
||||
.expect("fixed unrealized PnL overflow");
|
||||
total
|
||||
.checked_add(delta)
|
||||
.expect("fixed unrealized PnL overflow")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FixedAccount {
|
||||
pub cash: FixedMoney,
|
||||
pub units: FixedMoney,
|
||||
pub external_cash_flow_total: FixedMoney,
|
||||
}
|
||||
|
||||
impl FixedAccount {
|
||||
pub fn new(initial_cash: FixedMoney) -> Self {
|
||||
Self {
|
||||
cash: initial_cash,
|
||||
units: initial_cash,
|
||||
external_cash_flow_total: FixedMoney::ZERO,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_external_cash_flow(
|
||||
&mut self,
|
||||
amount: FixedMoney,
|
||||
unit_nav: FixedMoney,
|
||||
) -> Result<(), String> {
|
||||
if unit_nav.raw() <= 0 {
|
||||
return Err("fixed unit NAV must be positive".to_string());
|
||||
}
|
||||
let exact_units_raw = amount
|
||||
.raw()
|
||||
.checked_mul(MONEY_SCALE)
|
||||
.and_then(|value| value.checked_div(unit_nav.raw()))
|
||||
.ok_or_else(|| "fixed external flow unit conversion overflow".to_string())?;
|
||||
self.cash = self
|
||||
.cash
|
||||
.checked_add(amount)
|
||||
.ok_or_else(|| "fixed cash overflow".to_string())?;
|
||||
self.units = self
|
||||
.units
|
||||
.checked_add(FixedMoney::from_raw(exact_units_raw))
|
||||
.ok_or_else(|| "fixed units overflow".to_string())?;
|
||||
self.external_cash_flow_total = self
|
||||
.external_cash_flow_total
|
||||
.checked_add(amount)
|
||||
.ok_or_else(|| "fixed external flow overflow".to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn unit_nav(&self, total_equity: FixedMoney) -> Result<FixedMoney, String> {
|
||||
if self.units.raw() <= 0 {
|
||||
return Err("fixed account has no units".to_string());
|
||||
}
|
||||
let raw = total_equity
|
||||
.raw()
|
||||
.checked_mul(MONEY_SCALE)
|
||||
.and_then(|value| value.checked_div(self.units.raw()))
|
||||
.ok_or_else(|| "fixed unit NAV overflow".to_string())?;
|
||||
Ok(FixedMoney::from_raw(raw))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::cost::{ChinaAShareCostModel, CostModel};
|
||||
use crate::risk_control::TradingConstraintConfig;
|
||||
|
||||
fn fixed_model() -> FixedChinaAShareCostModel {
|
||||
let config = TradingConstraintConfig::default();
|
||||
FixedChinaAShareCostModel {
|
||||
commission_rate: FixedMoney::from_f64(config.commission_rate).unwrap(),
|
||||
stamp_tax_rate_before_change: FixedMoney::from_f64(config.stamp_tax_rate_before_change)
|
||||
.unwrap(),
|
||||
stamp_tax_rate_after_change: FixedMoney::from_f64(config.stamp_tax_rate_after_change)
|
||||
.unwrap(),
|
||||
stamp_tax_change_date: config.stamp_tax_change_date,
|
||||
minimum_commission: FixedMoney::from_f64(config.minimum_commission).unwrap(),
|
||||
transfer_fee_rate: FixedMoney::from_f64(config.transfer_fee_rate).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decimal_parser_rounds_only_beyond_money_scale() {
|
||||
assert_eq!(
|
||||
FixedMoney::from_decimal_str("1.234567").unwrap().raw(),
|
||||
1_234_567
|
||||
);
|
||||
assert_eq!(
|
||||
FixedMoney::from_decimal_str("1.2345675").unwrap().raw(),
|
||||
1_234_568
|
||||
);
|
||||
assert_eq!(
|
||||
FixedMoney::from_decimal_str("-0.0000014").unwrap().raw(),
|
||||
-1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_cost_model_matches_fixed_execution_primitive() {
|
||||
let fixed = fixed_model();
|
||||
let float = ChinaAShareCostModel::default();
|
||||
let dates = [
|
||||
NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
|
||||
NaiveDate::from_ymd_opt(2025, 1, 2).unwrap(),
|
||||
];
|
||||
for gross in [0.01, 10.0, 16_666.67, 248_059.812, 1_000_000.01] {
|
||||
let fixed_gross = FixedMoney::from_f64(gross).unwrap();
|
||||
for date in dates {
|
||||
for side in [OrderSide::Buy, OrderSide::Sell] {
|
||||
let expected = float.calculate(date, side, gross);
|
||||
let actual = fixed.calculate(date, side, fixed_gross);
|
||||
for (actual, expected) in [
|
||||
(actual.commission, expected.commission),
|
||||
(actual.stamp_tax, expected.stamp_tax),
|
||||
(actual.transfer_fee, expected.transfer_fee),
|
||||
] {
|
||||
assert_eq!(actual.to_f64(), expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_split_commission_matches_fixed_execution_primitive() {
|
||||
let fixed = fixed_model();
|
||||
let float = ChinaAShareCostModel::default();
|
||||
let mut fixed_state = BTreeMap::new();
|
||||
let mut float_state = BTreeMap::new();
|
||||
let mut fixed_total = FixedMoney::ZERO;
|
||||
let mut float_total = 0.0;
|
||||
for gross in [1000.0, 2000.0, 4000.0, 40_000.0] {
|
||||
let fixed_fee = fixed.commission_for_order_fill(
|
||||
FixedMoney::from_f64(gross).unwrap(),
|
||||
Some(42),
|
||||
&mut fixed_state,
|
||||
);
|
||||
let float_fee = float.commission_for_order_fill(gross, Some(42), &mut float_state);
|
||||
fixed_total = fixed_total.checked_add(fixed_fee).unwrap();
|
||||
float_total += float_fee;
|
||||
}
|
||||
assert_eq!(fixed_total.to_f64(), float_total);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixed_budget_never_exceeds_cash_after_cost() {
|
||||
let model = fixed_model();
|
||||
let date = NaiveDate::from_ymd_opt(2025, 2, 3).unwrap();
|
||||
let cash = FixedMoney::from_decimal_str("99880.00").unwrap();
|
||||
let price = FixedMoney::from_decimal_str("19.9731").unwrap();
|
||||
let mut quantity = 5_000u64;
|
||||
while quantity > 0 {
|
||||
let gross = price.checked_mul_quantity(quantity).unwrap();
|
||||
if gross
|
||||
.checked_add(model.calculate(date, OrderSide::Buy, gross).total())
|
||||
.unwrap()
|
||||
<= cash
|
||||
{
|
||||
break;
|
||||
}
|
||||
quantity -= 100;
|
||||
}
|
||||
let gross = price.checked_mul_quantity(quantity).unwrap();
|
||||
let total = gross
|
||||
.checked_add(model.calculate(date, OrderSide::Buy, gross).total())
|
||||
.unwrap();
|
||||
assert!(total <= cash);
|
||||
assert!(quantity < 5_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixed_budget_comparison_rejects_one_micro_yuan_overrun() {
|
||||
assert_eq!(FixedMoney::f64_fits_within(100.0, 100.0), Some(true));
|
||||
assert_eq!(FixedMoney::f64_fits_within(100.000001, 100.0), Some(false));
|
||||
assert_eq!(
|
||||
FixedMoney::f64_fits_within(100.000001, f64::INFINITY),
|
||||
Some(true)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixed_fifo_pnl_and_external_flow_are_deterministic() {
|
||||
let day_one = NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
|
||||
let day_two = NaiveDate::from_ymd_opt(2025, 1, 3).unwrap();
|
||||
let mut book = FixedLotBook::default();
|
||||
book.buy(day_one, 100, FixedMoney::from_decimal_str("10.01").unwrap());
|
||||
book.buy(day_two, 100, FixedMoney::from_decimal_str("10.03").unwrap());
|
||||
let realized = book
|
||||
.sell(150, FixedMoney::from_decimal_str("10.11").unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(realized.raw(), 14_000_000);
|
||||
assert_eq!(book.quantity, 50);
|
||||
assert_eq!(
|
||||
book.unrealized_pnl(FixedMoney::from_decimal_str("10.20").unwrap())
|
||||
.raw(),
|
||||
8_500_000
|
||||
);
|
||||
|
||||
let mut account = FixedAccount::new(FixedMoney::from_decimal_str("100.00").unwrap());
|
||||
account
|
||||
.apply_external_cash_flow(
|
||||
FixedMoney::from_decimal_str("50.00").unwrap(),
|
||||
FixedMoney::from_decimal_str("1.00").unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(account.units.raw(), 150 * MONEY_SCALE);
|
||||
assert_eq!(
|
||||
account
|
||||
.unit_nav(FixedMoney::from_decimal_str("150.00").unwrap())
|
||||
.unwrap()
|
||||
.raw(),
|
||||
MONEY_SCALE
|
||||
);
|
||||
assert_eq!(account.external_cash_flow_total.raw(), 50 * MONEY_SCALE);
|
||||
}
|
||||
}
|
||||
+321
-75
@@ -7,6 +7,24 @@ use crate::events::{
|
||||
AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent, ProcessEvent,
|
||||
ProcessEventKind,
|
||||
};
|
||||
use crate::fixed_point::FixedMoney;
|
||||
|
||||
fn futures_money(value: f64, label: &str) -> Result<FixedMoney, String> {
|
||||
FixedMoney::from_f64(value)
|
||||
.ok_or_else(|| format!("{label} is not representable as fixed-point money: {value}"))
|
||||
}
|
||||
|
||||
fn futures_money_or_panic(value: f64, label: &str) -> FixedMoney {
|
||||
futures_money(value, label).unwrap_or_else(|error| panic!("{error}"))
|
||||
}
|
||||
|
||||
fn sum_futures_money(values: impl IntoIterator<Item = FixedMoney>, label: &str) -> FixedMoney {
|
||||
values.into_iter().fold(FixedMoney::ZERO, |total, value| {
|
||||
total
|
||||
.checked_add(value)
|
||||
.unwrap_or_else(|| panic!("fixed-point {label} overflow"))
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum FuturesDirection {
|
||||
@@ -345,6 +363,14 @@ pub struct FuturesExecutionReport {
|
||||
}
|
||||
|
||||
impl FuturesContractSpec {
|
||||
pub fn unresolved() -> Self {
|
||||
Self {
|
||||
contract_multiplier: f64::NAN,
|
||||
long_margin_rate: f64::NAN,
|
||||
short_margin_rate: f64::NAN,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(contract_multiplier: f64, long_margin_rate: f64, short_margin_rate: f64) -> Self {
|
||||
Self {
|
||||
contract_multiplier: contract_multiplier.max(1.0),
|
||||
@@ -359,6 +385,15 @@ impl FuturesContractSpec {
|
||||
FuturesDirection::Short => self.short_margin_rate,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_resolved(&self) -> bool {
|
||||
self.contract_multiplier.is_finite()
|
||||
&& self.contract_multiplier > 0.0
|
||||
&& self.long_margin_rate.is_finite()
|
||||
&& self.long_margin_rate >= 0.0
|
||||
&& self.short_margin_rate.is_finite()
|
||||
&& self.short_margin_rate >= 0.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -366,15 +401,16 @@ pub struct FuturesPosition {
|
||||
pub symbol: String,
|
||||
pub direction: FuturesDirection,
|
||||
pub old_quantity: u32,
|
||||
day_start_quantity: u32,
|
||||
pub quantity: u32,
|
||||
pub avg_price: f64,
|
||||
pub last_price: f64,
|
||||
pub prev_close: f64,
|
||||
pub contract_multiplier: f64,
|
||||
pub margin_rate: f64,
|
||||
pub transaction_cost: f64,
|
||||
transaction_cost: FixedMoney,
|
||||
trade_quantity_delta: i32,
|
||||
trade_cost: f64,
|
||||
trade_value: FixedMoney,
|
||||
}
|
||||
|
||||
impl FuturesPosition {
|
||||
@@ -390,15 +426,16 @@ impl FuturesPosition {
|
||||
symbol: symbol.into(),
|
||||
direction,
|
||||
old_quantity: init_quantity,
|
||||
day_start_quantity: init_quantity,
|
||||
quantity: init_quantity,
|
||||
avg_price: init_price.max(0.0),
|
||||
last_price: init_price.max(0.0),
|
||||
prev_close: init_price.max(0.0),
|
||||
contract_multiplier: spec.contract_multiplier,
|
||||
margin_rate,
|
||||
transaction_cost: 0.0,
|
||||
transaction_cost: FixedMoney::ZERO,
|
||||
trade_quantity_delta: 0,
|
||||
trade_cost: 0.0,
|
||||
trade_value: FixedMoney::ZERO,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -407,18 +444,39 @@ impl FuturesPosition {
|
||||
}
|
||||
|
||||
pub fn market_value(&self) -> f64 {
|
||||
self.quantity as f64 * self.last_price * self.contract_multiplier
|
||||
self.market_value_money().to_f64()
|
||||
}
|
||||
|
||||
fn market_value_money(&self) -> FixedMoney {
|
||||
futures_money_or_panic(
|
||||
self.quantity as f64 * self.last_price * self.contract_multiplier,
|
||||
"futures position market value",
|
||||
)
|
||||
}
|
||||
|
||||
pub fn margin(&self) -> f64 {
|
||||
self.market_value() * self.margin_rate
|
||||
self.margin_money().to_f64()
|
||||
}
|
||||
|
||||
fn margin_money(&self) -> FixedMoney {
|
||||
futures_money_or_panic(
|
||||
self.market_value_money().to_f64() * self.margin_rate,
|
||||
"futures position margin",
|
||||
)
|
||||
}
|
||||
|
||||
pub fn equity(&self) -> f64 {
|
||||
(self.last_price - self.avg_price)
|
||||
* self.quantity as f64
|
||||
* self.contract_multiplier
|
||||
* self.direction.factor()
|
||||
self.equity_money().to_f64()
|
||||
}
|
||||
|
||||
fn equity_money(&self) -> FixedMoney {
|
||||
futures_money_or_panic(
|
||||
(self.last_price - self.avg_price)
|
||||
* self.quantity as f64
|
||||
* self.contract_multiplier
|
||||
* self.direction.factor(),
|
||||
"futures position equity",
|
||||
)
|
||||
}
|
||||
|
||||
pub fn pnl(&self) -> f64 {
|
||||
@@ -426,22 +484,47 @@ impl FuturesPosition {
|
||||
}
|
||||
|
||||
pub fn trading_pnl(&self) -> f64 {
|
||||
(self.trade_quantity_delta as f64 * self.last_price - self.trade_cost)
|
||||
* self.contract_multiplier
|
||||
* self.direction.factor()
|
||||
self.trading_pnl_money().to_f64()
|
||||
}
|
||||
|
||||
fn trading_pnl_money(&self) -> FixedMoney {
|
||||
let marked_trade_value = futures_money_or_panic(
|
||||
self.trade_quantity_delta as f64 * self.last_price * self.contract_multiplier,
|
||||
"futures marked trade value",
|
||||
);
|
||||
let pnl = marked_trade_value
|
||||
.checked_sub(self.trade_value)
|
||||
.expect("fixed-point futures trading PnL overflow");
|
||||
if self.direction == FuturesDirection::Short {
|
||||
pnl.checked_neg()
|
||||
.expect("fixed-point futures short trading PnL overflow")
|
||||
} else {
|
||||
pnl
|
||||
}
|
||||
}
|
||||
|
||||
pub fn position_pnl(&self) -> f64 {
|
||||
if self.old_quantity == 0 {
|
||||
0.0
|
||||
self.position_pnl_money().to_f64()
|
||||
}
|
||||
|
||||
fn position_pnl_money(&self) -> FixedMoney {
|
||||
if self.day_start_quantity == 0 {
|
||||
FixedMoney::ZERO
|
||||
} else {
|
||||
self.old_quantity as f64
|
||||
* (self.last_price - self.prev_close)
|
||||
* self.contract_multiplier
|
||||
* self.direction.factor()
|
||||
futures_money_or_panic(
|
||||
self.day_start_quantity as f64
|
||||
* (self.last_price - self.prev_close)
|
||||
* self.contract_multiplier
|
||||
* self.direction.factor(),
|
||||
"futures position daily PnL",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn transaction_cost(&self) -> f64 {
|
||||
self.transaction_cost.to_f64()
|
||||
}
|
||||
|
||||
pub fn open(&mut self, quantity: u32, price: f64, transaction_cost: f64) {
|
||||
if quantity == 0 {
|
||||
return;
|
||||
@@ -450,9 +533,20 @@ impl FuturesPosition {
|
||||
self.quantity += quantity;
|
||||
self.avg_price = (old_value + price * quantity as f64) / self.quantity as f64;
|
||||
self.last_price = price;
|
||||
self.transaction_cost += transaction_cost.max(0.0);
|
||||
let transaction_cost =
|
||||
futures_money_or_panic(transaction_cost.max(0.0), "futures open transaction cost");
|
||||
self.transaction_cost = self
|
||||
.transaction_cost
|
||||
.checked_add(transaction_cost)
|
||||
.expect("fixed-point futures transaction cost overflow");
|
||||
self.trade_quantity_delta += quantity as i32;
|
||||
self.trade_cost += price * quantity as f64;
|
||||
self.trade_value = self
|
||||
.trade_value
|
||||
.checked_add(futures_money_or_panic(
|
||||
price * quantity as f64 * self.contract_multiplier,
|
||||
"futures open trade value",
|
||||
))
|
||||
.expect("fixed-point futures trade value overflow");
|
||||
}
|
||||
|
||||
pub fn close(
|
||||
@@ -476,6 +570,17 @@ impl FuturesPosition {
|
||||
transaction_cost: f64,
|
||||
effect: FuturesPositionEffect,
|
||||
) -> Result<f64, String> {
|
||||
self.close_with_effect_money(quantity, price, transaction_cost, effect)
|
||||
.map(FixedMoney::to_f64)
|
||||
}
|
||||
|
||||
fn close_with_effect_money(
|
||||
&mut self,
|
||||
quantity: u32,
|
||||
price: f64,
|
||||
transaction_cost: f64,
|
||||
effect: FuturesPositionEffect,
|
||||
) -> Result<FixedMoney, String> {
|
||||
if effect == FuturesPositionEffect::Open {
|
||||
return Err("close_with_effect does not accept open effect".to_string());
|
||||
}
|
||||
@@ -489,7 +594,7 @@ impl FuturesPosition {
|
||||
));
|
||||
}
|
||||
if quantity == 0 {
|
||||
return Ok(0.0);
|
||||
return Ok(FixedMoney::ZERO);
|
||||
}
|
||||
match effect {
|
||||
FuturesPositionEffect::Open => unreachable!(),
|
||||
@@ -523,19 +628,34 @@ impl FuturesPosition {
|
||||
}
|
||||
}
|
||||
|
||||
let realized = (price - self.avg_price)
|
||||
* quantity as f64
|
||||
* self.contract_multiplier
|
||||
* self.direction.factor()
|
||||
- transaction_cost.max(0.0);
|
||||
let transaction_cost =
|
||||
futures_money(transaction_cost.max(0.0), "futures close transaction cost")?;
|
||||
let realized = futures_money(
|
||||
(price - self.avg_price)
|
||||
* quantity as f64
|
||||
* self.contract_multiplier
|
||||
* self.direction.factor(),
|
||||
"futures realized PnL",
|
||||
)?
|
||||
.checked_sub(transaction_cost)
|
||||
.ok_or_else(|| "fixed-point futures realized PnL overflow".to_string())?;
|
||||
self.quantity -= quantity;
|
||||
if self.quantity == 0 {
|
||||
self.avg_price = 0.0;
|
||||
}
|
||||
self.last_price = price;
|
||||
self.transaction_cost += transaction_cost.max(0.0);
|
||||
self.transaction_cost = self
|
||||
.transaction_cost
|
||||
.checked_add(transaction_cost)
|
||||
.ok_or_else(|| "fixed-point futures transaction cost overflow".to_string())?;
|
||||
self.trade_quantity_delta -= quantity as i32;
|
||||
self.trade_cost -= price * quantity as f64;
|
||||
self.trade_value = self
|
||||
.trade_value
|
||||
.checked_sub(futures_money(
|
||||
price * quantity as f64 * self.contract_multiplier,
|
||||
"futures close trade value",
|
||||
)?)
|
||||
.ok_or_else(|| "fixed-point futures trade value overflow".to_string())?;
|
||||
Ok(realized)
|
||||
}
|
||||
|
||||
@@ -547,98 +667,163 @@ impl FuturesPosition {
|
||||
|
||||
pub fn begin_trading_day(&mut self) {
|
||||
self.old_quantity = self.quantity;
|
||||
self.day_start_quantity = self.quantity;
|
||||
self.prev_close = self.last_price;
|
||||
self.transaction_cost = 0.0;
|
||||
self.transaction_cost = FixedMoney::ZERO;
|
||||
self.trade_quantity_delta = 0;
|
||||
self.trade_cost = 0.0;
|
||||
self.trade_value = FixedMoney::ZERO;
|
||||
}
|
||||
|
||||
pub fn settlement(&mut self, settlement_price: f64) -> f64 {
|
||||
self.settlement_money(settlement_price).to_f64()
|
||||
}
|
||||
|
||||
fn settlement_money(&mut self, settlement_price: f64) -> FixedMoney {
|
||||
self.mark_price(settlement_price);
|
||||
let cash_delta = self.equity();
|
||||
let cash_delta = self.equity_money();
|
||||
self.avg_price = self.last_price;
|
||||
self.prev_close = self.last_price;
|
||||
self.old_quantity = self.quantity;
|
||||
cash_delta
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FuturesAccountState {
|
||||
starting_cash: f64,
|
||||
total_cash: f64,
|
||||
frozen_cash: f64,
|
||||
starting_cash: FixedMoney,
|
||||
total_cash: FixedMoney,
|
||||
frozen_cash: FixedMoney,
|
||||
closed_day_trading_pnl: FixedMoney,
|
||||
closed_day_position_pnl: FixedMoney,
|
||||
closed_day_transaction_cost: FixedMoney,
|
||||
positions: BTreeMap<(String, FuturesDirection), FuturesPosition>,
|
||||
}
|
||||
|
||||
impl FuturesAccountState {
|
||||
pub fn new(total_cash: f64) -> Self {
|
||||
let total_cash = futures_money_or_panic(total_cash, "futures starting cash");
|
||||
Self {
|
||||
starting_cash: total_cash,
|
||||
total_cash,
|
||||
frozen_cash: 0.0,
|
||||
frozen_cash: FixedMoney::ZERO,
|
||||
closed_day_trading_pnl: FixedMoney::ZERO,
|
||||
closed_day_position_pnl: FixedMoney::ZERO,
|
||||
closed_day_transaction_cost: FixedMoney::ZERO,
|
||||
positions: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn starting_cash(&self) -> f64 {
|
||||
self.starting_cash
|
||||
self.starting_cash.to_f64()
|
||||
}
|
||||
|
||||
pub fn total_cash(&self) -> f64 {
|
||||
self.total_cash
|
||||
self.total_cash.to_f64()
|
||||
}
|
||||
|
||||
pub fn frozen_cash(&self) -> f64 {
|
||||
self.frozen_cash
|
||||
self.frozen_cash.to_f64()
|
||||
}
|
||||
|
||||
pub fn cash(&self) -> f64 {
|
||||
self.total_cash - self.margin() - self.frozen_cash
|
||||
self.cash_money().to_f64()
|
||||
}
|
||||
|
||||
fn cash_money(&self) -> FixedMoney {
|
||||
self.total_cash
|
||||
.checked_sub(self.margin_money())
|
||||
.and_then(|cash| cash.checked_sub(self.frozen_cash))
|
||||
.expect("fixed-point futures available cash overflow")
|
||||
}
|
||||
|
||||
pub fn margin(&self) -> f64 {
|
||||
self.positions.values().map(FuturesPosition::margin).sum()
|
||||
self.margin_money().to_f64()
|
||||
}
|
||||
|
||||
fn margin_money(&self) -> FixedMoney {
|
||||
sum_futures_money(
|
||||
self.positions.values().map(FuturesPosition::margin_money),
|
||||
"futures account margin",
|
||||
)
|
||||
}
|
||||
|
||||
pub fn market_value(&self) -> f64 {
|
||||
self.positions
|
||||
.values()
|
||||
.map(FuturesPosition::market_value)
|
||||
.sum()
|
||||
sum_futures_money(
|
||||
self.positions
|
||||
.values()
|
||||
.map(FuturesPosition::market_value_money),
|
||||
"futures account market value",
|
||||
)
|
||||
.to_f64()
|
||||
}
|
||||
|
||||
pub fn position_equity(&self) -> f64 {
|
||||
self.positions.values().map(FuturesPosition::equity).sum()
|
||||
self.position_equity_money().to_f64()
|
||||
}
|
||||
|
||||
fn position_equity_money(&self) -> FixedMoney {
|
||||
sum_futures_money(
|
||||
self.positions.values().map(FuturesPosition::equity_money),
|
||||
"futures account position equity",
|
||||
)
|
||||
}
|
||||
|
||||
pub fn total_value(&self) -> f64 {
|
||||
self.total_cash + self.position_equity()
|
||||
self.total_cash
|
||||
.checked_add(self.position_equity_money())
|
||||
.expect("fixed-point futures total value overflow")
|
||||
.to_f64()
|
||||
}
|
||||
|
||||
pub fn daily_pnl(&self) -> f64 {
|
||||
self.trading_pnl() + self.position_pnl() - self.transaction_cost()
|
||||
self.trading_pnl_money()
|
||||
.checked_add(self.position_pnl_money())
|
||||
.and_then(|pnl| pnl.checked_sub(self.transaction_cost_money()))
|
||||
.expect("fixed-point futures daily PnL overflow")
|
||||
.to_f64()
|
||||
}
|
||||
|
||||
pub fn trading_pnl(&self) -> f64 {
|
||||
self.positions
|
||||
.values()
|
||||
.map(FuturesPosition::trading_pnl)
|
||||
.sum()
|
||||
self.trading_pnl_money().to_f64()
|
||||
}
|
||||
|
||||
fn trading_pnl_money(&self) -> FixedMoney {
|
||||
sum_futures_money(
|
||||
std::iter::once(self.closed_day_trading_pnl).chain(
|
||||
self.positions
|
||||
.values()
|
||||
.map(FuturesPosition::trading_pnl_money),
|
||||
),
|
||||
"futures account trading PnL",
|
||||
)
|
||||
}
|
||||
|
||||
pub fn position_pnl(&self) -> f64 {
|
||||
self.positions
|
||||
.values()
|
||||
.map(FuturesPosition::position_pnl)
|
||||
.sum()
|
||||
self.position_pnl_money().to_f64()
|
||||
}
|
||||
|
||||
fn position_pnl_money(&self) -> FixedMoney {
|
||||
sum_futures_money(
|
||||
std::iter::once(self.closed_day_position_pnl).chain(
|
||||
self.positions
|
||||
.values()
|
||||
.map(FuturesPosition::position_pnl_money),
|
||||
),
|
||||
"futures account position PnL",
|
||||
)
|
||||
}
|
||||
|
||||
pub fn transaction_cost(&self) -> f64 {
|
||||
self.positions
|
||||
.values()
|
||||
.map(|position| position.transaction_cost)
|
||||
.sum()
|
||||
self.transaction_cost_money().to_f64()
|
||||
}
|
||||
|
||||
fn transaction_cost_money(&self) -> FixedMoney {
|
||||
sum_futures_money(
|
||||
std::iter::once(self.closed_day_transaction_cost).chain(
|
||||
self.positions
|
||||
.values()
|
||||
.map(|position| position.transaction_cost),
|
||||
),
|
||||
"futures account transaction cost",
|
||||
)
|
||||
}
|
||||
|
||||
pub fn positions(&self) -> &BTreeMap<(String, FuturesDirection), FuturesPosition> {
|
||||
@@ -667,7 +852,13 @@ impl FuturesAccountState {
|
||||
.entry((symbol.clone(), direction))
|
||||
.or_insert_with(|| FuturesPosition::new(symbol, direction, spec, 0, price));
|
||||
position.open(quantity, price, transaction_cost);
|
||||
self.total_cash -= transaction_cost.max(0.0);
|
||||
self.total_cash = self
|
||||
.total_cash
|
||||
.checked_sub(futures_money_or_panic(
|
||||
transaction_cost.max(0.0),
|
||||
"futures open transaction cost",
|
||||
))
|
||||
.expect("fixed-point futures cash overflow");
|
||||
}
|
||||
|
||||
pub fn close(
|
||||
@@ -702,12 +893,30 @@ impl FuturesAccountState {
|
||||
.positions
|
||||
.get_mut(&key)
|
||||
.ok_or_else(|| format!("missing futures position {symbol} {}", direction.as_str()))?;
|
||||
let cash_delta = position.close_with_effect(quantity, price, transaction_cost, effect)?;
|
||||
self.total_cash += cash_delta;
|
||||
let cash_delta =
|
||||
position.close_with_effect_money(quantity, price, transaction_cost, effect)?;
|
||||
self.total_cash = self
|
||||
.total_cash
|
||||
.checked_add(cash_delta)
|
||||
.ok_or_else(|| "fixed-point futures cash overflow".to_string())?;
|
||||
if position.quantity == 0 {
|
||||
self.closed_day_trading_pnl = self
|
||||
.closed_day_trading_pnl
|
||||
.checked_add(position.trading_pnl_money())
|
||||
.ok_or_else(|| "fixed-point closed futures trading PnL overflow".to_string())?;
|
||||
self.closed_day_position_pnl = self
|
||||
.closed_day_position_pnl
|
||||
.checked_add(position.position_pnl_money())
|
||||
.ok_or_else(|| "fixed-point closed futures position PnL overflow".to_string())?;
|
||||
self.closed_day_transaction_cost = self
|
||||
.closed_day_transaction_cost
|
||||
.checked_add(position.transaction_cost)
|
||||
.ok_or_else(|| {
|
||||
"fixed-point closed futures transaction cost overflow".to_string()
|
||||
})?;
|
||||
self.positions.remove(&key);
|
||||
}
|
||||
Ok(cash_delta)
|
||||
Ok(cash_delta.to_f64())
|
||||
}
|
||||
|
||||
pub fn execute_order(
|
||||
@@ -746,6 +955,9 @@ impl FuturesAccountState {
|
||||
);
|
||||
report.order_events.push(OrderEvent {
|
||||
date,
|
||||
decision_date: None,
|
||||
order_created_date: None,
|
||||
execution_date: None,
|
||||
order_id,
|
||||
symbol: intent.symbol,
|
||||
side,
|
||||
@@ -779,7 +991,7 @@ impl FuturesAccountState {
|
||||
intent.price,
|
||||
intent.transaction_cost,
|
||||
);
|
||||
if projected.cash() < -1e-8 {
|
||||
if projected.cash_money().raw() < 0 {
|
||||
Err(format!(
|
||||
"insufficient futures margin available_cash={:.2} required_margin_after={:.2}",
|
||||
self.cash(),
|
||||
@@ -794,7 +1006,13 @@ impl FuturesAccountState {
|
||||
intent.price,
|
||||
intent.transaction_cost,
|
||||
);
|
||||
Ok(-intent.transaction_cost.max(0.0))
|
||||
Ok(futures_money_or_panic(
|
||||
intent.transaction_cost.max(0.0),
|
||||
"futures open transaction cost",
|
||||
)
|
||||
.checked_neg()
|
||||
.expect("fixed-point futures open cash delta overflow")
|
||||
.to_f64())
|
||||
}
|
||||
}
|
||||
FuturesPositionEffect::Close
|
||||
@@ -819,18 +1037,32 @@ impl FuturesAccountState {
|
||||
.position(&intent.symbol, intent.direction)
|
||||
.map(|position| position.avg_price)
|
||||
.unwrap_or(0.0);
|
||||
let notional =
|
||||
intent.price * intent.quantity as f64 * intent.spec.contract_multiplier;
|
||||
let notional = futures_money_or_panic(
|
||||
intent.price * intent.quantity as f64 * intent.spec.contract_multiplier,
|
||||
"futures fill notional",
|
||||
)
|
||||
.to_f64();
|
||||
let transaction_cost = futures_money_or_panic(
|
||||
intent.transaction_cost.max(0.0),
|
||||
"futures fill transaction cost",
|
||||
)
|
||||
.to_f64();
|
||||
report.fill_events.push(FillEvent {
|
||||
date,
|
||||
decision_date: None,
|
||||
order_created_date: None,
|
||||
execution_date: None,
|
||||
execution_start_timestamp: None,
|
||||
execution_timestamp: None,
|
||||
order_id,
|
||||
symbol: intent.symbol.clone(),
|
||||
side,
|
||||
quantity: intent.quantity,
|
||||
price: intent.price,
|
||||
gross_amount: notional,
|
||||
commission: intent.transaction_cost.max(0.0),
|
||||
commission: transaction_cost,
|
||||
stamp_tax: 0.0,
|
||||
transfer_fee: 0.0,
|
||||
net_cash_flow: cash_delta,
|
||||
reason: format!(
|
||||
"{} direction={} effect={}",
|
||||
@@ -889,6 +1121,9 @@ impl FuturesAccountState {
|
||||
});
|
||||
report.order_events.push(OrderEvent {
|
||||
date,
|
||||
decision_date: None,
|
||||
order_created_date: None,
|
||||
execution_date: None,
|
||||
order_id,
|
||||
symbol: intent.symbol,
|
||||
side,
|
||||
@@ -915,6 +1150,9 @@ impl FuturesAccountState {
|
||||
);
|
||||
report.order_events.push(OrderEvent {
|
||||
date,
|
||||
decision_date: None,
|
||||
order_created_date: None,
|
||||
execution_date: None,
|
||||
order_id,
|
||||
symbol: intent.symbol,
|
||||
side,
|
||||
@@ -997,22 +1235,30 @@ impl FuturesAccountState {
|
||||
}
|
||||
|
||||
pub fn begin_trading_day(&mut self) {
|
||||
self.closed_day_trading_pnl = FixedMoney::ZERO;
|
||||
self.closed_day_position_pnl = FixedMoney::ZERO;
|
||||
self.closed_day_transaction_cost = FixedMoney::ZERO;
|
||||
for position in self.positions.values_mut() {
|
||||
position.begin_trading_day();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn settle(&mut self, settlement_prices: &BTreeMap<String, f64>) -> f64 {
|
||||
let mut cash_delta = 0.0;
|
||||
let mut cash_delta = FixedMoney::ZERO;
|
||||
for position in self.positions.values_mut() {
|
||||
let price = settlement_prices
|
||||
.get(&position.symbol)
|
||||
.copied()
|
||||
.unwrap_or(position.last_price);
|
||||
cash_delta += position.settlement(price);
|
||||
cash_delta = cash_delta
|
||||
.checked_add(position.settlement_money(price))
|
||||
.expect("fixed-point futures settlement overflow");
|
||||
}
|
||||
self.total_cash += cash_delta;
|
||||
cash_delta
|
||||
self.total_cash = self
|
||||
.total_cash
|
||||
.checked_add(cash_delta)
|
||||
.expect("fixed-point futures cash settlement overflow");
|
||||
cash_delta.to_f64()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
use chrono::NaiveDate;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub fn listed_sector_is_kcb(value: &str) -> Option<bool> {
|
||||
match value.trim().to_ascii_uppercase().as_str() {
|
||||
"科创板" | "KSH" | "STAR" | "STAR_MARKET" => Some(true),
|
||||
"主板" | "沪市主板" | "深市主板" | "中小板" | "中小企业板" | "创业板"
|
||||
| "北交所" | "北证" | "新三板" | "基础层" | "创新层" | "精选层"
|
||||
| "MAIN" | "MAIN_BOARD" | "CHINEXT" | "GEM" | "BJ" | "BJS" | "BJSE"
|
||||
| "BSE" => Some(false),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Instrument {
|
||||
pub symbol: String,
|
||||
@@ -21,17 +32,29 @@ impl Instrument {
|
||||
}
|
||||
|
||||
pub fn minimum_order_quantity(&self) -> u32 {
|
||||
match self.board.trim().to_ascii_uppercase().as_str() {
|
||||
"KSH" => 200,
|
||||
"BJS" | "BJ" | "BJSE" => 100,
|
||||
_ => self.effective_round_lot(),
|
||||
let board = self.board.trim();
|
||||
if board.eq_ignore_ascii_case("KSH") {
|
||||
200
|
||||
} else if board.eq_ignore_ascii_case("BJS")
|
||||
|| board.eq_ignore_ascii_case("BJ")
|
||||
|| board.eq_ignore_ascii_case("BJSE")
|
||||
{
|
||||
100
|
||||
} else {
|
||||
self.effective_round_lot()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn order_step_size(&self) -> u32 {
|
||||
match self.board.trim().to_ascii_uppercase().as_str() {
|
||||
"KSH" | "BJS" | "BJ" | "BJSE" => 1,
|
||||
_ => self.effective_round_lot(),
|
||||
let board = self.board.trim();
|
||||
if board.eq_ignore_ascii_case("KSH")
|
||||
|| board.eq_ignore_ascii_case("BJS")
|
||||
|| board.eq_ignore_ascii_case("BJ")
|
||||
|| board.eq_ignore_ascii_case("BJSE")
|
||||
{
|
||||
1
|
||||
} else {
|
||||
self.effective_round_lot()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,10 +63,15 @@ impl Instrument {
|
||||
.is_some_and(|delisted_at| delisted_at < date)
|
||||
}
|
||||
|
||||
pub fn is_delisted_on_or_before(&self, date: NaiveDate) -> bool {
|
||||
self.delisted_at
|
||||
.is_some_and(|delisted_at| delisted_at <= date)
|
||||
}
|
||||
|
||||
pub fn is_active_on(&self, date: NaiveDate) -> bool {
|
||||
self.listed_at.is_none_or(|listed_at| listed_at <= date)
|
||||
&& !self.is_delisted_before(date)
|
||||
&& !self.status.eq_ignore_ascii_case("inactive")
|
||||
&& !(self.status.eq_ignore_ascii_case("inactive") && self.delisted_at.is_none())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +79,50 @@ fn default_status() -> String {
|
||||
"active".to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Instrument, listed_sector_is_kcb};
|
||||
|
||||
#[test]
|
||||
fn listing_sector_is_explicit_and_unknown_stays_unknown() {
|
||||
assert_eq!(listed_sector_is_kcb("科创板"), Some(true));
|
||||
assert_eq!(listed_sector_is_kcb(" star "), Some(true));
|
||||
assert_eq!(listed_sector_is_kcb("主板"), Some(false));
|
||||
assert_eq!(listed_sector_is_kcb("创业板"), Some(false));
|
||||
assert_eq!(listed_sector_is_kcb("北证"), Some(false));
|
||||
for value in ["", "-", "SH", "688001.SH", "半导体"] {
|
||||
assert_eq!(listed_sector_is_kcb(value), None);
|
||||
}
|
||||
}
|
||||
|
||||
fn instrument(board: &str, round_lot: u32) -> Instrument {
|
||||
Instrument {
|
||||
symbol: "000001.SZ".to_string(),
|
||||
name: "test".to_string(),
|
||||
board: board.to_string(),
|
||||
round_lot,
|
||||
listed_at: None,
|
||||
delisted_at: None,
|
||||
status: "active".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn order_quantity_rules_are_case_insensitive_without_allocating_normalized_boards() {
|
||||
let kcb = instrument(" kSh ", 100);
|
||||
assert_eq!(kcb.minimum_order_quantity(), 200);
|
||||
assert_eq!(kcb.order_step_size(), 1);
|
||||
|
||||
let bjse = instrument("bjse", 100);
|
||||
assert_eq!(bjse.minimum_order_quantity(), 100);
|
||||
assert_eq!(bjse.order_step_size(), 1);
|
||||
|
||||
let main_board = instrument("SZSE", 50);
|
||||
assert_eq!(main_board.minimum_order_quantity(), 50);
|
||||
assert_eq!(main_board.order_step_size(), 50);
|
||||
}
|
||||
}
|
||||
|
||||
mod optional_date_format {
|
||||
use chrono::NaiveDate;
|
||||
use serde::{self, Deserialize, Deserializer, Serializer};
|
||||
|
||||
+36
-14
@@ -5,51 +5,66 @@ pub mod data;
|
||||
pub mod engine;
|
||||
pub mod event_bus;
|
||||
pub mod events;
|
||||
pub mod fixed_point;
|
||||
pub mod futures;
|
||||
pub mod instrument;
|
||||
pub mod metrics;
|
||||
mod numeric_expr_vm;
|
||||
pub mod platform_expr_strategy;
|
||||
pub mod platform_runtime_schema;
|
||||
pub mod platform_strategy_spec;
|
||||
pub mod portfolio;
|
||||
pub mod risk_control;
|
||||
pub mod rules;
|
||||
pub mod scheduler;
|
||||
pub mod strategy;
|
||||
pub mod strategy_ai;
|
||||
pub mod universe;
|
||||
|
||||
pub use broker::{BrokerExecutionReport, BrokerSimulator, MatchingType, SlippageModel};
|
||||
pub use broker::{
|
||||
BrokerExecutionReport, BrokerSimulator, DynamicSlippageConfig, MatchingType, RebalanceCashMode,
|
||||
SlippageModel,
|
||||
};
|
||||
pub use calendar::TradingCalendar;
|
||||
pub use cost::{ChinaAShareCostModel, CostModel, TradingCost};
|
||||
pub use data::{
|
||||
BenchmarkSnapshot, CandidateEligibility, CorporateAction, DailyFactorSnapshot,
|
||||
DailyMarketSnapshot, DailySnapshotBundle, DataSet, DataSetError, DividendRecord,
|
||||
EligibleUniverseSnapshot, FactorTextValue, FactorValue, IntradayExecutionQuote,
|
||||
IntradayOrderBookDepthLevel, PriceBar, PriceField, SecuritiesMarginRecord, SplitRecord,
|
||||
YieldCurvePoint,
|
||||
IntradayMarketSnapshotOverlay, IntradayOrderBookDepthLevel, NumericFactorMap, PriceBar,
|
||||
PriceField, SecuritiesMarginRecord, SplitRecord, YieldCurvePoint,
|
||||
};
|
||||
pub use engine::{
|
||||
AnalyzerMonthlyReturnRow, AnalyzerPositionRow, AnalyzerReport, AnalyzerRiskSummary,
|
||||
AnalyzerTradeRow, BacktestConfig, BacktestDayProgress, BacktestEngine, BacktestError,
|
||||
BacktestResult, DailyEquityPoint, FuturesValidationConfig,
|
||||
BacktestResult, BacktestTerminalAssetClass, BacktestTerminalAudit, BacktestTerminalOpenOrder,
|
||||
BacktestTerminalStatus, DailyEquityPoint, ExecutionQuoteRequest, FuturesValidationConfig,
|
||||
ProcessEventRetention, backtest_execution_dates,
|
||||
};
|
||||
pub use event_bus::{BacktestProcessMod, BacktestProcessModLoader, ProcessEventBus};
|
||||
pub use events::{
|
||||
AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent, ProcessEvent,
|
||||
ProcessEventKind,
|
||||
};
|
||||
pub use fixed_point::{
|
||||
FixedAccount, FixedChinaAShareCostModel, FixedLotBook, FixedMoney, FixedTradingCost,
|
||||
MONEY_SCALE,
|
||||
};
|
||||
pub use futures::{
|
||||
FuturesAccountState, FuturesCommissionType, FuturesContractSpec, FuturesDirection,
|
||||
FuturesExecutionReport, FuturesOrderIntent, FuturesPosition, FuturesPositionEffect,
|
||||
FuturesTradingParameter, FuturesTransactionCostModel,
|
||||
};
|
||||
pub use instrument::Instrument;
|
||||
pub use metrics::{BacktestMetrics, compute_backtest_metrics};
|
||||
pub use metrics::{
|
||||
BacktestMetrics, RiskFreeRateContract, RiskFreeRateObservation, compute_backtest_metrics,
|
||||
};
|
||||
pub use platform_expr_strategy::{
|
||||
PlatformAccountActionKind, PlatformExplicitActionStage, PlatformExplicitCancelKind,
|
||||
PlatformExplicitOrderKind, PlatformExprStrategy, PlatformExprStrategyConfig,
|
||||
PlatformRebalanceSchedule, PlatformScheduleFrequency, PlatformTradeAction,
|
||||
PlatformUniverseActionKind,
|
||||
PlatformPortfolioDrawdownControlConfig, PlatformPositionTargetRule, PlatformRebalanceSchedule,
|
||||
PlatformScheduleFrequency, PlatformSelectionQuotePlan, PlatformStopTakeReferencePriceMode,
|
||||
PlatformTradeAction, PlatformUniverseActionKind,
|
||||
};
|
||||
pub use platform_runtime_schema::{
|
||||
PLATFORM_RUNTIME_SCHEMA_VERSION, PlatformRuntimeSchema, reserved_scope_names,
|
||||
@@ -62,24 +77,31 @@ pub use platform_strategy_spec::{
|
||||
StrategyExpressionActionConfig, StrategyExpressionAllocationConfig,
|
||||
StrategyExpressionOrderingConfig, StrategyExpressionRiskConfig,
|
||||
StrategyExpressionScheduleConfig, StrategyExpressionSelectionConfig,
|
||||
StrategyExpressionTradingConfig, StrategyRuntimeEnvironment, StrategyRuntimeExpressions,
|
||||
StrategyRuntimeSpec, platform_expr_config_from_spec, platform_expr_config_from_value,
|
||||
StrategyExpressionTradingConfig, StrategyPortfolioDrawdownControlConfig, StrategyRebalanceSpec,
|
||||
StrategyRiskPolicySpec, StrategyRuntimeEnvironment, StrategyRuntimeExpressions,
|
||||
StrategyRuntimeSpec, StrategyUniverseSpec, platform_expr_config_from_spec,
|
||||
platform_expr_config_from_value, validate_strategy_risk_policy_fields,
|
||||
};
|
||||
pub use portfolio::{CashReceivable, HoldingSummary, PendingCashFlow, PortfolioState, Position};
|
||||
pub use risk_control::{
|
||||
ChinaAShareRiskControl, FidcRiskControlConfig, FidcRiskDecisionAudit, RiskCheckScope,
|
||||
StaticRiskRuleConfig, TradingConstraintConfig,
|
||||
};
|
||||
pub use rules::{ChinaEquityRuleHooks, EquityRuleHooks, RuleCheck};
|
||||
pub use scheduler::{
|
||||
ScheduleFrequency, ScheduleRule, ScheduleStage, ScheduleTimeRule, Scheduler, default_stage_time,
|
||||
};
|
||||
pub use strategy::{
|
||||
AlgoOrderStyle, CnSmallCapRotationConfig, CnSmallCapRotationStrategy, OmniMicroCapConfig,
|
||||
OmniMicroCapStrategy, OpenOrderView, OrderIntent, OrderRuntimeView, PortfolioRuntimeView,
|
||||
Strategy, StrategyContext, StrategyDecision, TargetPortfolioOrderPricing,
|
||||
OmniMicroCapStrategy, OpenOrderView, OrderIntent, OrderRuntimeView, OrderTimeInForce,
|
||||
PortfolioRuntimeView, Strategy, StrategyContext, StrategyDecision, TargetPortfolioOrderPricing,
|
||||
};
|
||||
pub use strategy_ai::{
|
||||
ManualExample, ManualFactorSource, ManualField, ManualFieldGroup, ManualFunction,
|
||||
ManualSection, StrategyAiCatalog, StrategyAiGenerateRequest, StrategyAiManual,
|
||||
StrategyAiOptimizeRequest, build_generation_prompt, build_optimization_prompt,
|
||||
built_in_strategy_manual, merge_catalog_into_manual, render_manual_markdown,
|
||||
ManualSection, StrategyAiCatalog, StrategyAiGenerateRequest, StrategyAiHoldingCountContract,
|
||||
StrategyAiManual, StrategyAiOptimizeRequest, build_generation_prompt,
|
||||
build_optimization_prompt, built_in_strategy_manual, merge_catalog_into_manual,
|
||||
render_manual_markdown,
|
||||
};
|
||||
pub use universe::{
|
||||
BandRegime, DynamicMarketCapBandSelector, SelectionContext, SelectionDiagnostics,
|
||||
|
||||
+444
-57
@@ -4,12 +4,33 @@ use chrono::{Datelike, NaiveDate};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::engine::DailyEquityPoint;
|
||||
use crate::events::FillEvent;
|
||||
use crate::events::{AccountEvent, FillEvent};
|
||||
use crate::portfolio::HoldingSummary;
|
||||
|
||||
const TRADING_DAYS_PER_YEAR: f64 = 252.0;
|
||||
const MONTHS_PER_YEAR: f64 = 12.0;
|
||||
const DEFAULT_RISK_FREE_RATE: f64 = 0.022;
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RiskFreeRateObservation {
|
||||
pub date: NaiveDate,
|
||||
pub source_date: NaiveDate,
|
||||
pub annual_rate: f64,
|
||||
pub daily_rate: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RiskFreeRateContract {
|
||||
pub version: String,
|
||||
pub source: String,
|
||||
pub tenor: String,
|
||||
pub periods_per_year: f64,
|
||||
pub max_staleness_days: usize,
|
||||
pub observed_max_staleness_days: usize,
|
||||
pub sha256: String,
|
||||
pub observations: Vec<RiskFreeRateObservation>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct BacktestMetrics {
|
||||
@@ -29,6 +50,7 @@ pub struct BacktestMetrics {
|
||||
pub max_drawdown_duration_days: usize,
|
||||
pub total_trade_days: usize,
|
||||
pub sortino: f64,
|
||||
pub downside_risk: f64,
|
||||
pub information_ratio: f64,
|
||||
pub tracking_error: f64,
|
||||
pub volatility: f64,
|
||||
@@ -47,30 +69,42 @@ pub struct BacktestMetrics {
|
||||
pub cash_balance: f64,
|
||||
pub unit_nav: f64,
|
||||
pub initial_cash: f64,
|
||||
/// Sum of external deposits (positive) and withdrawals (negative). This
|
||||
/// is reported separately so callers cannot mistake a cash transfer for
|
||||
/// trading performance.
|
||||
#[serde(default)]
|
||||
pub external_cash_flow_total: f64,
|
||||
pub excess_win_rate: f64,
|
||||
pub monthly_sharpe: f64,
|
||||
pub monthly_volatility: f64,
|
||||
pub risk_free_rate_contract_version: String,
|
||||
pub risk_free_rate_source: String,
|
||||
pub risk_free_rate_tenor: String,
|
||||
pub risk_free_rate_observation_count: usize,
|
||||
pub risk_free_rate_max_staleness_days: usize,
|
||||
pub risk_free_rate_observed_max_staleness_days: usize,
|
||||
pub risk_free_rate_sha256: String,
|
||||
}
|
||||
|
||||
pub fn compute_backtest_metrics(
|
||||
equity_curve: &[DailyEquityPoint],
|
||||
fills: &[FillEvent],
|
||||
daily_holdings: &[HoldingSummary],
|
||||
account_events: &[AccountEvent],
|
||||
initial_cash: f64,
|
||||
) -> BacktestMetrics {
|
||||
risk_free_contract: Option<&RiskFreeRateContract>,
|
||||
) -> Result<BacktestMetrics, String> {
|
||||
let Some(first_point) = equity_curve.first() else {
|
||||
return BacktestMetrics {
|
||||
risk_free_rate: DEFAULT_RISK_FREE_RATE,
|
||||
return Ok(BacktestMetrics {
|
||||
initial_cash,
|
||||
..BacktestMetrics::default()
|
||||
};
|
||||
});
|
||||
};
|
||||
let Some(last_point) = equity_curve.last() else {
|
||||
return BacktestMetrics {
|
||||
risk_free_rate: DEFAULT_RISK_FREE_RATE,
|
||||
return Ok(BacktestMetrics {
|
||||
initial_cash,
|
||||
..BacktestMetrics::default()
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
let trade_days = equity_curve.len();
|
||||
@@ -81,12 +115,28 @@ pub fn compute_backtest_metrics(
|
||||
} else {
|
||||
first_point.benchmark_close
|
||||
};
|
||||
let mut returns = Vec::with_capacity(equity_curve.len());
|
||||
returns.push(pct_change(initial_cash, first_point.total_equity));
|
||||
returns.extend(
|
||||
let explicit_unit_nav = equity_curve.iter().any(|point| {
|
||||
point.external_cash_flow.abs() > f64::EPSILON
|
||||
|| (point.unit_nav.is_finite()
|
||||
&& point.unit_nav > 0.0
|
||||
&& (point.unit_nav - safe_div(point.total_equity, initial_cash, 1.0)).abs() > 1e-12)
|
||||
});
|
||||
let portfolio_nav = if explicit_unit_nav {
|
||||
equity_curve
|
||||
.iter()
|
||||
.map(|point| point_nav(point, initial_cash))
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
flow_neutral_nav_series(equity_curve, account_events, initial_cash)
|
||||
};
|
||||
let mut returns = Vec::with_capacity(portfolio_nav.len());
|
||||
if let Some(first_nav) = portfolio_nav.first().copied() {
|
||||
returns.push(pct_change(1.0, first_nav));
|
||||
}
|
||||
returns.extend(
|
||||
portfolio_nav
|
||||
.windows(2)
|
||||
.map(|window| pct_change(window[0].total_equity, window[1].total_equity)),
|
||||
.map(|window| pct_change(window[0], window[1])),
|
||||
);
|
||||
let mut benchmark_returns = Vec::with_capacity(equity_curve.len());
|
||||
benchmark_returns.push(pct_change(benchmark_start, first_point.benchmark_close));
|
||||
@@ -100,6 +150,7 @@ pub fn compute_backtest_metrics(
|
||||
.zip(benchmark_returns.iter())
|
||||
.map(|(lhs, rhs)| lhs - rhs)
|
||||
.collect::<Vec<_>>();
|
||||
let zero_risk_free_rates = vec![0.0; excess_returns.len()];
|
||||
|
||||
let benchmark_net_value = if benchmark_start.abs() < f64::EPSILON {
|
||||
1.0
|
||||
@@ -107,35 +158,41 @@ pub fn compute_backtest_metrics(
|
||||
last_point.benchmark_close / benchmark_start
|
||||
};
|
||||
let benchmark_cumulative_return = benchmark_net_value - 1.0;
|
||||
let total_return = if initial_cash.abs() < f64::EPSILON {
|
||||
0.0
|
||||
} else {
|
||||
(last_point.total_equity / initial_cash) - 1.0
|
||||
};
|
||||
let final_nav = portfolio_nav.last().copied().unwrap_or(1.0);
|
||||
let total_return = final_nav - 1.0;
|
||||
let excess_cumulative_return = if benchmark_net_value.abs() < f64::EPSILON {
|
||||
total_return
|
||||
} else {
|
||||
(last_point.total_equity / initial_cash) / benchmark_net_value - 1.0
|
||||
portfolio_nav.last().copied().unwrap_or(0.0) / benchmark_net_value - 1.0
|
||||
};
|
||||
let excess_return = total_return - benchmark_cumulative_return;
|
||||
let annual_return = annualize_return(total_return, trade_days);
|
||||
let excess_annual_return = annualize_return(excess_cumulative_return, trade_days);
|
||||
|
||||
let risk_free_rate = DEFAULT_RISK_FREE_RATE;
|
||||
let daily_rf = risk_free_rate / TRADING_DAYS_PER_YEAR;
|
||||
let sharpe = annualized_sharpe(&returns, daily_rf, TRADING_DAYS_PER_YEAR);
|
||||
let sortino = annualized_sortino(&returns, daily_rf, TRADING_DAYS_PER_YEAR);
|
||||
let information_ratio = annualized_sharpe(&excess_returns, 0.0, TRADING_DAYS_PER_YEAR);
|
||||
let (daily_risk_free_rates, risk_free_metadata) =
|
||||
aligned_daily_risk_free_rates(equity_curve, risk_free_contract)?;
|
||||
let risk_free_rate =
|
||||
effective_annual_risk_free_rate(&daily_risk_free_rates, TRADING_DAYS_PER_YEAR);
|
||||
let sharpe = annualized_sharpe(&returns, &daily_risk_free_rates, TRADING_DAYS_PER_YEAR);
|
||||
let sortino = annualized_sortino(&returns, &daily_risk_free_rates, TRADING_DAYS_PER_YEAR);
|
||||
let downside_risk =
|
||||
annualized_downside_risk(&returns, &daily_risk_free_rates, TRADING_DAYS_PER_YEAR);
|
||||
let information_ratio = annualized_sharpe(
|
||||
&excess_returns,
|
||||
&zero_risk_free_rates,
|
||||
TRADING_DAYS_PER_YEAR,
|
||||
);
|
||||
let tracking_error = annualized_std(&excess_returns, TRADING_DAYS_PER_YEAR);
|
||||
let volatility = annualized_std(&returns, TRADING_DAYS_PER_YEAR);
|
||||
let excess_volatility = annualized_std(&excess_returns, TRADING_DAYS_PER_YEAR);
|
||||
let excess_sharpe = annualized_sharpe(&excess_returns, 0.0, TRADING_DAYS_PER_YEAR);
|
||||
let (alpha, beta) = alpha_beta(&returns, &benchmark_returns, daily_rf);
|
||||
let excess_sharpe = annualized_sharpe(
|
||||
&excess_returns,
|
||||
&zero_risk_free_rates,
|
||||
TRADING_DAYS_PER_YEAR,
|
||||
);
|
||||
let (alpha, beta) = alpha_beta(&returns, &benchmark_returns, &daily_risk_free_rates);
|
||||
|
||||
let equity_nav = equity_curve
|
||||
.iter()
|
||||
.map(|point| safe_div(point.total_equity, initial_cash, 1.0))
|
||||
.collect::<Vec<_>>();
|
||||
let equity_nav = portfolio_nav;
|
||||
let benchmark_nav_series = equity_curve
|
||||
.iter()
|
||||
.map(|point| safe_div(point.benchmark_close, benchmark_start, 1.0))
|
||||
@@ -154,8 +211,7 @@ pub fn compute_backtest_metrics(
|
||||
let win_rate = ratio(winning_days, returns.len());
|
||||
let excess_win_rate = ratio(excess_winning_days, excess_returns.len());
|
||||
|
||||
let monthly_portfolio_returns =
|
||||
group_monthly_returns(equity_curve, initial_cash, |point| point.total_equity);
|
||||
let monthly_portfolio_returns = group_monthly_returns_from_values(equity_curve, &equity_nav);
|
||||
let monthly_benchmark_returns =
|
||||
group_monthly_returns(equity_curve, benchmark_start, |point| point.benchmark_close);
|
||||
let monthly_excess_returns = monthly_portfolio_returns
|
||||
@@ -163,6 +219,8 @@ pub fn compute_backtest_metrics(
|
||||
.zip(monthly_benchmark_returns.iter())
|
||||
.map(|(lhs, rhs)| lhs - rhs)
|
||||
.collect::<Vec<_>>();
|
||||
let monthly_risk_free_returns =
|
||||
group_monthly_risk_free_returns(equity_curve, &daily_risk_free_rates);
|
||||
let monthly_excess_win_rate = ratio(
|
||||
monthly_excess_returns
|
||||
.iter()
|
||||
@@ -172,7 +230,7 @@ pub fn compute_backtest_metrics(
|
||||
);
|
||||
let monthly_sharpe = annualized_sharpe(
|
||||
&monthly_portfolio_returns,
|
||||
risk_free_rate / MONTHS_PER_YEAR,
|
||||
&monthly_risk_free_returns,
|
||||
MONTHS_PER_YEAR,
|
||||
);
|
||||
let monthly_volatility = annualized_std(&monthly_portfolio_returns, MONTHS_PER_YEAR);
|
||||
@@ -224,7 +282,7 @@ pub fn compute_backtest_metrics(
|
||||
|
||||
let total_trade_days = equity_by_date.len();
|
||||
|
||||
BacktestMetrics {
|
||||
Ok(BacktestMetrics {
|
||||
total_return,
|
||||
annual_return,
|
||||
sharpe,
|
||||
@@ -241,6 +299,7 @@ pub fn compute_backtest_metrics(
|
||||
max_drawdown_duration_days,
|
||||
total_trade_days,
|
||||
sortino,
|
||||
downside_risk,
|
||||
information_ratio,
|
||||
tracking_error,
|
||||
volatility,
|
||||
@@ -257,11 +316,34 @@ pub fn compute_backtest_metrics(
|
||||
average_daily_turnover,
|
||||
total_assets: last_point.total_equity,
|
||||
cash_balance: last_point.cash,
|
||||
unit_nav: safe_div(last_point.total_equity, initial_cash, 0.0),
|
||||
unit_nav: final_nav,
|
||||
initial_cash,
|
||||
external_cash_flow_total: if explicit_unit_nav {
|
||||
equity_curve
|
||||
.iter()
|
||||
.map(|point| point.external_cash_flow)
|
||||
.sum()
|
||||
} else {
|
||||
external_flow_total_from_events(account_events)
|
||||
},
|
||||
excess_win_rate,
|
||||
monthly_sharpe,
|
||||
monthly_volatility,
|
||||
risk_free_rate_contract_version: risk_free_metadata.version,
|
||||
risk_free_rate_source: risk_free_metadata.source,
|
||||
risk_free_rate_tenor: risk_free_metadata.tenor,
|
||||
risk_free_rate_observation_count: daily_risk_free_rates.len(),
|
||||
risk_free_rate_max_staleness_days: risk_free_metadata.max_staleness_days,
|
||||
risk_free_rate_observed_max_staleness_days: risk_free_metadata.observed_max_staleness_days,
|
||||
risk_free_rate_sha256: risk_free_metadata.sha256,
|
||||
})
|
||||
}
|
||||
|
||||
fn point_nav(point: &DailyEquityPoint, initial_cash: f64) -> f64 {
|
||||
if point.unit_nav.is_finite() && point.unit_nav > 0.0 {
|
||||
point.unit_nav
|
||||
} else {
|
||||
safe_div(point.total_equity, initial_cash, 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,13 +367,106 @@ fn annualize_return(total_return: f64, periods: usize) -> f64 {
|
||||
base.powf(TRADING_DAYS_PER_YEAR / periods) - 1.0
|
||||
}
|
||||
|
||||
fn annualized_sharpe(returns: &[f64], daily_rf: f64, periods_per_year: f64) -> f64 {
|
||||
if returns.len() < 2 {
|
||||
fn aligned_daily_risk_free_rates(
|
||||
equity_curve: &[DailyEquityPoint],
|
||||
contract: Option<&RiskFreeRateContract>,
|
||||
) -> Result<(Vec<f64>, RiskFreeRateContract), String> {
|
||||
let Some(contract) = contract else {
|
||||
return Ok((
|
||||
vec![0.0; equity_curve.len()],
|
||||
RiskFreeRateContract {
|
||||
version: "not-configured".to_string(),
|
||||
source: "not-configured".to_string(),
|
||||
tenor: "NONE".to_string(),
|
||||
periods_per_year: TRADING_DAYS_PER_YEAR,
|
||||
..RiskFreeRateContract::default()
|
||||
},
|
||||
));
|
||||
};
|
||||
if contract.version.trim().is_empty()
|
||||
|| contract.source.trim().is_empty()
|
||||
|| contract.tenor.trim().is_empty()
|
||||
|| contract.sha256.len() != 64
|
||||
{
|
||||
return Err("risk-free rate contract metadata is incomplete".to_string());
|
||||
}
|
||||
if contract.observations.len() != equity_curve.len() {
|
||||
return Err(format!(
|
||||
"risk-free rate observation count mismatch: expected={} actual={}",
|
||||
equity_curve.len(),
|
||||
contract.observations.len()
|
||||
));
|
||||
}
|
||||
let mut rates = Vec::with_capacity(equity_curve.len());
|
||||
for (point, observation) in equity_curve.iter().zip(&contract.observations) {
|
||||
if observation.date != point.date {
|
||||
return Err(format!(
|
||||
"risk-free rate date mismatch: expected={} actual={}",
|
||||
point.date, observation.date
|
||||
));
|
||||
}
|
||||
if observation.source_date > observation.date {
|
||||
return Err(format!(
|
||||
"risk-free rate uses future observation: date={} source_date={}",
|
||||
observation.date, observation.source_date
|
||||
));
|
||||
}
|
||||
let staleness = observation
|
||||
.date
|
||||
.signed_duration_since(observation.source_date)
|
||||
.num_days();
|
||||
if staleness < 0 || staleness as usize > contract.max_staleness_days {
|
||||
return Err(format!(
|
||||
"risk-free rate observation is stale: date={} source_date={} days={}",
|
||||
observation.date, observation.source_date, staleness
|
||||
));
|
||||
}
|
||||
if !observation.annual_rate.is_finite()
|
||||
|| observation.annual_rate <= -1.0
|
||||
|| observation.annual_rate >= 1.0
|
||||
|| !observation.daily_rate.is_finite()
|
||||
|| observation.daily_rate <= -1.0
|
||||
{
|
||||
return Err(format!(
|
||||
"risk-free rate observation is invalid: date={}",
|
||||
observation.date
|
||||
));
|
||||
}
|
||||
let periods_per_year =
|
||||
if contract.periods_per_year.is_finite() && contract.periods_per_year > 0.0 {
|
||||
contract.periods_per_year
|
||||
} else {
|
||||
TRADING_DAYS_PER_YEAR
|
||||
};
|
||||
let expected_daily = (1.0 + observation.annual_rate).powf(1.0 / periods_per_year) - 1.0;
|
||||
if (expected_daily - observation.daily_rate).abs() > 1e-12 {
|
||||
return Err(format!(
|
||||
"risk-free daily conversion mismatch: date={} expected={} actual={}",
|
||||
observation.date, expected_daily, observation.daily_rate
|
||||
));
|
||||
}
|
||||
rates.push(observation.daily_rate);
|
||||
}
|
||||
Ok((rates, contract.clone()))
|
||||
}
|
||||
|
||||
fn effective_annual_risk_free_rate(daily_rates: &[f64], periods_per_year: f64) -> f64 {
|
||||
if daily_rates.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
let mean_log =
|
||||
daily_rates.iter().map(|rate| rate.ln_1p()).sum::<f64>() / daily_rates.len() as f64;
|
||||
(mean_log * periods_per_year).exp_m1()
|
||||
}
|
||||
|
||||
fn annualized_sharpe(returns: &[f64], daily_risk_free_rates: &[f64], periods_per_year: f64) -> f64 {
|
||||
if returns.len() < 2 || returns.len() != daily_risk_free_rates.len() {
|
||||
return 0.0;
|
||||
}
|
||||
let adjusted = returns
|
||||
.iter()
|
||||
.map(|value| value - daily_rf)
|
||||
.zip(daily_risk_free_rates)
|
||||
.map(|(value, risk_free)| value - risk_free)
|
||||
.collect::<Vec<_>>();
|
||||
let mean_ret = mean(&adjusted);
|
||||
let std = std_dev(&adjusted);
|
||||
@@ -302,23 +477,24 @@ fn annualized_sharpe(returns: &[f64], daily_rf: f64, periods_per_year: f64) -> f
|
||||
}
|
||||
}
|
||||
|
||||
fn annualized_sortino(returns: &[f64], daily_rf: f64, periods_per_year: f64) -> f64 {
|
||||
if returns.is_empty() {
|
||||
fn annualized_sortino(
|
||||
returns: &[f64],
|
||||
daily_risk_free_rates: &[f64],
|
||||
periods_per_year: f64,
|
||||
) -> f64 {
|
||||
if returns.is_empty() || returns.len() != daily_risk_free_rates.len() {
|
||||
return 0.0;
|
||||
}
|
||||
let adjusted = returns
|
||||
.iter()
|
||||
.map(|value| value - daily_rf)
|
||||
.zip(daily_risk_free_rates)
|
||||
.map(|(value, risk_free)| value - risk_free)
|
||||
.collect::<Vec<_>>();
|
||||
let downside = adjusted
|
||||
.iter()
|
||||
.filter(|value| **value < 0.0)
|
||||
.map(|value| value.powi(2))
|
||||
.collect::<Vec<_>>();
|
||||
if downside.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
let downside_dev = (downside.iter().sum::<f64>() / downside.len() as f64).sqrt();
|
||||
.map(|value| value.min(0.0).powi(2))
|
||||
.sum::<f64>();
|
||||
let downside_dev = (downside / adjusted.len() as f64).sqrt();
|
||||
if downside_dev <= f64::EPSILON {
|
||||
0.0
|
||||
} else {
|
||||
@@ -326,32 +502,60 @@ fn annualized_sortino(returns: &[f64], daily_rf: f64, periods_per_year: f64) ->
|
||||
}
|
||||
}
|
||||
|
||||
fn annualized_downside_risk(
|
||||
returns: &[f64],
|
||||
daily_risk_free_rates: &[f64],
|
||||
periods_per_year: f64,
|
||||
) -> f64 {
|
||||
if returns.is_empty() || returns.len() != daily_risk_free_rates.len() {
|
||||
return 0.0;
|
||||
}
|
||||
let downside_mean_square = returns
|
||||
.iter()
|
||||
.zip(daily_risk_free_rates)
|
||||
.map(|(value, risk_free)| (value - risk_free).min(0.0).powi(2))
|
||||
.sum::<f64>()
|
||||
/ returns.len() as f64;
|
||||
downside_mean_square.sqrt() * periods_per_year.sqrt()
|
||||
}
|
||||
|
||||
fn annualized_std(values: &[f64], periods_per_year: f64) -> f64 {
|
||||
std_dev(values) * periods_per_year.sqrt()
|
||||
}
|
||||
|
||||
fn alpha_beta(returns: &[f64], benchmark_returns: &[f64], daily_rf: f64) -> (f64, f64) {
|
||||
if returns.len() < 2 || returns.len() != benchmark_returns.len() {
|
||||
fn alpha_beta(
|
||||
returns: &[f64],
|
||||
benchmark_returns: &[f64],
|
||||
daily_risk_free_rates: &[f64],
|
||||
) -> (f64, f64) {
|
||||
if returns.len() < 2
|
||||
|| returns.len() != benchmark_returns.len()
|
||||
|| returns.len() != daily_risk_free_rates.len()
|
||||
{
|
||||
return (0.0, 0.0);
|
||||
}
|
||||
let strategy_excess = returns
|
||||
.iter()
|
||||
.map(|value| value - daily_rf)
|
||||
.zip(daily_risk_free_rates)
|
||||
.map(|(value, risk_free)| value - risk_free)
|
||||
.collect::<Vec<_>>();
|
||||
let benchmark_excess = benchmark_returns
|
||||
.iter()
|
||||
.map(|value| value - daily_rf)
|
||||
.zip(daily_risk_free_rates)
|
||||
.map(|(value, risk_free)| value - risk_free)
|
||||
.collect::<Vec<_>>();
|
||||
let mean_strategy = mean(&strategy_excess);
|
||||
let mean_benchmark = mean(&benchmark_excess);
|
||||
let variance_benchmark = variance(&benchmark_excess);
|
||||
let mean_raw_strategy = mean(returns);
|
||||
let mean_raw_benchmark = mean(benchmark_returns);
|
||||
let variance_benchmark = variance(benchmark_returns);
|
||||
if variance_benchmark <= f64::EPSILON {
|
||||
return (0.0, 0.0);
|
||||
}
|
||||
let covariance = strategy_excess
|
||||
let covariance = returns
|
||||
.iter()
|
||||
.zip(benchmark_excess.iter())
|
||||
.map(|(lhs, rhs)| (lhs - mean_strategy) * (rhs - mean_benchmark))
|
||||
.zip(benchmark_returns.iter())
|
||||
.map(|(lhs, rhs)| (lhs - mean_raw_strategy) * (rhs - mean_raw_benchmark))
|
||||
.sum::<f64>()
|
||||
/ (strategy_excess.len() - 1) as f64;
|
||||
let beta = covariance / variance_benchmark;
|
||||
@@ -384,6 +588,80 @@ fn drawdown_stats(nav: &[f64]) -> (f64, usize) {
|
||||
(max_drawdown, max_duration)
|
||||
}
|
||||
|
||||
fn flow_neutral_nav_series(
|
||||
equity_curve: &[DailyEquityPoint],
|
||||
account_events: &[AccountEvent],
|
||||
initial_cash: f64,
|
||||
) -> Vec<f64> {
|
||||
let mut external_flow_by_date = BTreeMap::<NaiveDate, f64>::new();
|
||||
for event in account_events {
|
||||
if !(event.note.starts_with("deposit_withdraw amount=")
|
||||
|| event.note.starts_with("deposit_withdraw_settled amount="))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
*external_flow_by_date.entry(event.date).or_default() +=
|
||||
event.cash_after - event.cash_before;
|
||||
}
|
||||
|
||||
let mut units = initial_cash;
|
||||
let mut previous_equity = initial_cash;
|
||||
let mut navs = Vec::with_capacity(equity_curve.len());
|
||||
for point in equity_curve {
|
||||
let unit_nav_before_flow = safe_div(previous_equity, units, 1.0);
|
||||
let external_flow = external_flow_by_date
|
||||
.get(&point.date)
|
||||
.copied()
|
||||
.unwrap_or_default();
|
||||
if external_flow.abs() > f64::EPSILON && unit_nav_before_flow.is_finite() {
|
||||
units += external_flow / unit_nav_before_flow;
|
||||
}
|
||||
let unit_nav = safe_div(point.total_equity, units, 0.0);
|
||||
navs.push(unit_nav);
|
||||
previous_equity = point.total_equity;
|
||||
}
|
||||
navs
|
||||
}
|
||||
|
||||
fn external_flow_total_from_events(account_events: &[AccountEvent]) -> f64 {
|
||||
account_events
|
||||
.iter()
|
||||
.filter(|event| {
|
||||
event.note.starts_with("deposit_withdraw amount=")
|
||||
|| event.note.starts_with("deposit_withdraw_settled amount=")
|
||||
})
|
||||
.map(|event| event.cash_after - event.cash_before)
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn group_monthly_returns_from_values(
|
||||
equity_curve: &[DailyEquityPoint],
|
||||
values: &[f64],
|
||||
) -> Vec<f64> {
|
||||
let mut month_last = BTreeMap::<(i32, u32), f64>::new();
|
||||
let mut month_first = BTreeMap::<(i32, u32), f64>::new();
|
||||
let mut previous_value = 1.0;
|
||||
for (point, value) in equity_curve.iter().zip(values.iter().copied()) {
|
||||
let key = (point.date.year(), point.date.month());
|
||||
month_first.entry(key).or_insert(previous_value);
|
||||
month_last.insert(key, value);
|
||||
previous_value = value;
|
||||
}
|
||||
let mut keys = month_last.keys().copied().collect::<Vec<_>>();
|
||||
keys.sort_unstable();
|
||||
keys.into_iter()
|
||||
.filter_map(|key| {
|
||||
let first = month_first.get(&key).copied().unwrap_or_default();
|
||||
let last = month_last.get(&key).copied().unwrap_or_default();
|
||||
if first.abs() < f64::EPSILON {
|
||||
None
|
||||
} else {
|
||||
Some((last / first) - 1.0)
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn group_monthly_returns<F>(
|
||||
equity_curve: &[DailyEquityPoint],
|
||||
initial_value: f64,
|
||||
@@ -417,6 +695,26 @@ where
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn group_monthly_risk_free_returns(
|
||||
equity_curve: &[DailyEquityPoint],
|
||||
daily_risk_free_rates: &[f64],
|
||||
) -> Vec<f64> {
|
||||
if equity_curve.len() != daily_risk_free_rates.len() {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut monthly_growth = BTreeMap::<(i32, u32), f64>::new();
|
||||
for (point, daily_rate) in equity_curve.iter().zip(daily_risk_free_rates) {
|
||||
let growth = monthly_growth
|
||||
.entry((point.date.year(), point.date.month()))
|
||||
.or_insert(1.0);
|
||||
*growth *= 1.0 + daily_rate;
|
||||
}
|
||||
monthly_growth
|
||||
.into_values()
|
||||
.map(|growth| growth - 1.0)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn mean(values: &[f64]) -> f64 {
|
||||
if values.is_empty() {
|
||||
0.0
|
||||
@@ -486,6 +784,8 @@ mod tests {
|
||||
cash: total_equity,
|
||||
market_value: 0.0,
|
||||
total_equity,
|
||||
external_cash_flow: 0.0,
|
||||
unit_nav: total_equity / 100.0,
|
||||
benchmark_close,
|
||||
benchmark_prev_close,
|
||||
notes: String::new(),
|
||||
@@ -499,8 +799,95 @@ mod tests {
|
||||
equity_point("2025-01-02", 100.0, 5797.089, 5957.717),
|
||||
equity_point("2025-12-31", 120.0, 7595.285, 7597.299),
|
||||
];
|
||||
let metrics = compute_backtest_metrics(&curve, &[], &[], 100.0);
|
||||
let metrics = compute_backtest_metrics(&curve, &[], &[], &[], 100.0, None).unwrap();
|
||||
let expected = 7595.285 / 5957.717 - 1.0;
|
||||
assert!((metrics.benchmark_cumulative_return - expected).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_cash_flow_is_excluded_from_return_and_reported_separately() {
|
||||
let curve = vec![
|
||||
equity_point("2025-01-02", 100.0, 100.0, 100.0),
|
||||
DailyEquityPoint {
|
||||
date: NaiveDate::from_ymd_opt(2025, 1, 3).unwrap(),
|
||||
cash: 220.0,
|
||||
market_value: 0.0,
|
||||
total_equity: 220.0,
|
||||
external_cash_flow: 100.0,
|
||||
unit_nav: 1.1,
|
||||
benchmark_close: 100.0,
|
||||
benchmark_prev_close: 100.0,
|
||||
notes: String::new(),
|
||||
diagnostics: String::new(),
|
||||
},
|
||||
];
|
||||
let events = vec![AccountEvent {
|
||||
date: NaiveDate::from_ymd_opt(2025, 1, 3).unwrap(),
|
||||
cash_before: 100.0,
|
||||
cash_after: 200.0,
|
||||
total_equity: 200.0,
|
||||
note: "deposit_withdraw amount=100.00 reason=test".to_string(),
|
||||
}];
|
||||
let metrics = compute_backtest_metrics(&curve, &[], &[], &events, 100.0, None).unwrap();
|
||||
assert!((metrics.total_return - 0.1).abs() < 1e-12);
|
||||
assert!((metrics.unit_nav - 1.1).abs() < 1e-12);
|
||||
assert!((metrics.external_cash_flow_total - 100.0).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn risk_adjusted_metrics_use_daily_pit_rates_and_all_period_downside() {
|
||||
let curve = vec![
|
||||
equity_point("2026-01-02", 101.0, 100.0, 100.0),
|
||||
equity_point("2026-01-05", 98.98, 100.0, 100.0),
|
||||
equity_point("2026-01-06", 100.4647, 100.0, 100.0),
|
||||
equity_point("2026-01-07", 99.9623765, 100.0, 100.0),
|
||||
];
|
||||
let annual_rates = [0.012, 0.012, 0.013, 0.013];
|
||||
let observations = curve
|
||||
.iter()
|
||||
.zip(annual_rates)
|
||||
.map(|(point, annual_rate)| RiskFreeRateObservation {
|
||||
date: point.date,
|
||||
source_date: point.date,
|
||||
annual_rate,
|
||||
daily_rate: (1.0 + annual_rate).powf(1.0 / TRADING_DAYS_PER_YEAR) - 1.0,
|
||||
})
|
||||
.collect();
|
||||
let contract = RiskFreeRateContract {
|
||||
version: "cn-government-bond-3m-pit-daily/v1".to_string(),
|
||||
source: "test".to_string(),
|
||||
tenor: "3M".to_string(),
|
||||
periods_per_year: TRADING_DAYS_PER_YEAR,
|
||||
max_staleness_days: 15,
|
||||
observed_max_staleness_days: 0,
|
||||
sha256: "a".repeat(64),
|
||||
observations,
|
||||
};
|
||||
let metrics =
|
||||
compute_backtest_metrics(&curve, &[], &[], &[], 100.0, Some(&contract)).unwrap();
|
||||
|
||||
let returns = [0.01, -0.02, 0.015, -0.005];
|
||||
let daily_rates = annual_rates
|
||||
.map(|annual_rate| (1.0 + annual_rate).powf(1.0 / TRADING_DAYS_PER_YEAR) - 1.0);
|
||||
let adjusted = returns
|
||||
.iter()
|
||||
.zip(daily_rates)
|
||||
.map(|(value, risk_free)| value - risk_free)
|
||||
.collect::<Vec<_>>();
|
||||
let expected_sharpe = mean(&adjusted) / std_dev(&adjusted) * TRADING_DAYS_PER_YEAR.sqrt();
|
||||
let downside = (adjusted
|
||||
.iter()
|
||||
.map(|value| value.min(0.0).powi(2))
|
||||
.sum::<f64>()
|
||||
/ adjusted.len() as f64)
|
||||
.sqrt();
|
||||
let expected_sortino = mean(&adjusted) / downside * TRADING_DAYS_PER_YEAR.sqrt();
|
||||
assert!((metrics.sharpe - expected_sharpe).abs() < 1e-12);
|
||||
assert!((metrics.sortino - expected_sortino).abs() < 1e-12);
|
||||
assert!((metrics.downside_risk - downside * TRADING_DAYS_PER_YEAR.sqrt()).abs() < 1e-12);
|
||||
assert_eq!(metrics.risk_free_rate_source, "test");
|
||||
assert_eq!(metrics.risk_free_rate_tenor, "3M");
|
||||
assert_eq!(metrics.risk_free_rate_observation_count, 4);
|
||||
assert_ne!(metrics.risk_free_rate, 0.022);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+30155
-1359
File diff suppressed because it is too large
Load Diff
@@ -67,6 +67,7 @@ const RESERVED_SCOPE_NAMES: &[&str] = &[
|
||||
// day-level
|
||||
"signal_close",
|
||||
"benchmark_close",
|
||||
"benchmark_signal_close",
|
||||
"signal_ma5",
|
||||
"signal_ma10",
|
||||
"signal_ma20",
|
||||
@@ -135,11 +136,12 @@ const RESERVED_SCOPE_NAMES: &[&str] = &[
|
||||
"free_float_cap",
|
||||
"pe_ttm",
|
||||
"volume",
|
||||
"tick_volume",
|
||||
"minute_volume",
|
||||
"bid1_volume",
|
||||
"ask1_volume",
|
||||
"turnover_ratio",
|
||||
"effective_turnover_ratio",
|
||||
"up_days_stock",
|
||||
"open",
|
||||
"high",
|
||||
"low",
|
||||
@@ -154,7 +156,9 @@ const RESERVED_SCOPE_NAMES: &[&str] = &[
|
||||
"round_lot",
|
||||
"paused",
|
||||
"is_st",
|
||||
"is_star_st",
|
||||
"is_kcb",
|
||||
"is_bjse",
|
||||
"is_one_yuan",
|
||||
"is_new_listing",
|
||||
"allow_buy",
|
||||
@@ -224,6 +228,8 @@ const RUNTIME_HELPER_FUNCTIONS: &[&str] = &[
|
||||
"day_factor",
|
||||
"rolling_mean",
|
||||
"rolling_mean_current",
|
||||
"rolling_max_current",
|
||||
"rolling_return_stddev_current",
|
||||
"ma",
|
||||
"sma",
|
||||
"vma",
|
||||
@@ -323,10 +329,12 @@ mod tests {
|
||||
for required in [
|
||||
"signal_close",
|
||||
"benchmark_close",
|
||||
"benchmark_signal_close",
|
||||
"close",
|
||||
"avg_cost",
|
||||
"current_price",
|
||||
"stock_ma_short",
|
||||
"up_days_stock",
|
||||
] {
|
||||
assert!(
|
||||
names.contains(required),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+1009
-168
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@ use chrono::NaiveDate;
|
||||
|
||||
use crate::data::{CandidateEligibility, DailyMarketSnapshot, PriceField};
|
||||
use crate::portfolio::Position;
|
||||
use crate::risk_control::ChinaAShareRiskControl;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RuleCheck {
|
||||
@@ -26,6 +27,10 @@ impl RuleCheck {
|
||||
}
|
||||
|
||||
pub trait EquityRuleHooks {
|
||||
fn duplicates_standard_china_risk(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn can_buy(
|
||||
&self,
|
||||
execution_date: NaiveDate,
|
||||
@@ -47,21 +52,11 @@ pub trait EquityRuleHooks {
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ChinaEquityRuleHooks;
|
||||
|
||||
impl ChinaEquityRuleHooks {
|
||||
fn at_upper_limit(snapshot: &DailyMarketSnapshot, price_field: PriceField) -> bool {
|
||||
snapshot.is_at_upper_limit_price(snapshot.buy_price(price_field))
|
||||
}
|
||||
|
||||
fn at_lower_limit(snapshot: &DailyMarketSnapshot, price_field: PriceField) -> bool {
|
||||
let check_price = match price_field {
|
||||
PriceField::Last => snapshot.price(PriceField::Last),
|
||||
_ => snapshot.sell_price(price_field),
|
||||
};
|
||||
snapshot.is_at_lower_limit_price(check_price)
|
||||
}
|
||||
}
|
||||
|
||||
impl EquityRuleHooks for ChinaEquityRuleHooks {
|
||||
fn duplicates_standard_china_risk(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn can_buy(
|
||||
&self,
|
||||
_execution_date: NaiveDate,
|
||||
@@ -69,14 +64,14 @@ impl EquityRuleHooks for ChinaEquityRuleHooks {
|
||||
candidate: &CandidateEligibility,
|
||||
price_field: PriceField,
|
||||
) -> RuleCheck {
|
||||
if snapshot.paused || candidate.is_paused {
|
||||
return RuleCheck::reject("paused");
|
||||
}
|
||||
if !candidate.allow_buy {
|
||||
return RuleCheck::reject("buy disabled by eligibility flags");
|
||||
}
|
||||
if Self::at_upper_limit(snapshot, price_field) {
|
||||
return RuleCheck::reject("open at or above upper limit");
|
||||
if let Some(reason) = ChinaAShareRiskControl::buy_rejection_reason(
|
||||
_execution_date,
|
||||
candidate,
|
||||
snapshot,
|
||||
None,
|
||||
ChinaAShareRiskControl::buy_check_price(snapshot, price_field),
|
||||
) {
|
||||
return RuleCheck::reject(reason);
|
||||
}
|
||||
|
||||
RuleCheck::allow()
|
||||
@@ -90,17 +85,15 @@ impl EquityRuleHooks for ChinaEquityRuleHooks {
|
||||
position: &Position,
|
||||
price_field: PriceField,
|
||||
) -> RuleCheck {
|
||||
if snapshot.paused || candidate.is_paused {
|
||||
return RuleCheck::reject("paused");
|
||||
}
|
||||
if !candidate.allow_sell {
|
||||
return RuleCheck::reject("sell disabled by eligibility flags");
|
||||
}
|
||||
if Self::at_lower_limit(snapshot, price_field) {
|
||||
return RuleCheck::reject("open at or below lower limit");
|
||||
}
|
||||
if position.sellable_qty(execution_date) == 0 {
|
||||
return RuleCheck::reject("t+1 sellable quantity is zero");
|
||||
if let Some(reason) = ChinaAShareRiskControl::sell_rejection_reason(
|
||||
execution_date,
|
||||
candidate,
|
||||
snapshot,
|
||||
None,
|
||||
Some(position),
|
||||
ChinaAShareRiskControl::sell_check_price(snapshot, price_field),
|
||||
) {
|
||||
return RuleCheck::reject(reason);
|
||||
}
|
||||
|
||||
RuleCheck::allow()
|
||||
|
||||
@@ -7,7 +7,7 @@ pub enum ScheduleStage {
|
||||
BeforeTrading,
|
||||
OpenAuction,
|
||||
Bar,
|
||||
Tick,
|
||||
Minute,
|
||||
OnDay,
|
||||
AfterTrading,
|
||||
Settlement,
|
||||
@@ -164,6 +164,16 @@ impl<'a> Scheduler<'a> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Evaluate only the trading-calendar frequency of a rule.
|
||||
///
|
||||
/// Strategy callbacks and order execution clocks are separate contracts:
|
||||
/// a 15:00 schedule is still due on the same daily/weekly/monthly trading
|
||||
/// date even when the engine's coarse `on_day` callback runs at another
|
||||
/// default time. Exact clock matching remains in `triggered_rules_at`.
|
||||
pub fn is_due_on(&self, date: NaiveDate, rule: &ScheduleRule) -> bool {
|
||||
self.matches(date, rule)
|
||||
}
|
||||
|
||||
fn matches(&self, date: NaiveDate, rule: &ScheduleRule) -> bool {
|
||||
match &rule.frequency {
|
||||
ScheduleFrequency::Daily => true,
|
||||
@@ -225,7 +235,7 @@ pub fn default_stage_time(stage: ScheduleStage) -> Option<NaiveTime> {
|
||||
ScheduleStage::BeforeTrading => Some(NaiveTime::from_hms_opt(9, 0, 0).expect("valid time")),
|
||||
ScheduleStage::OpenAuction => Some(NaiveTime::from_hms_opt(9, 31, 0).expect("valid time")),
|
||||
ScheduleStage::Bar => Some(NaiveTime::from_hms_opt(10, 18, 0).expect("valid time")),
|
||||
ScheduleStage::Tick => None,
|
||||
ScheduleStage::Minute => None,
|
||||
ScheduleStage::OnDay => Some(NaiveTime::from_hms_opt(10, 18, 0).expect("valid time")),
|
||||
ScheduleStage::AfterTrading => Some(NaiveTime::from_hms_opt(15, 0, 0).expect("valid time")),
|
||||
ScheduleStage::Settlement => Some(NaiveTime::from_hms_opt(15, 1, 0).expect("valid time")),
|
||||
@@ -265,6 +275,38 @@ mod tests {
|
||||
])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn date_due_is_independent_from_the_order_execution_clock() {
|
||||
let calendar = sample_calendar();
|
||||
let scheduler = Scheduler::new(&calendar);
|
||||
let daily = ScheduleRule::daily("close_signal", ScheduleStage::OnDay)
|
||||
.with_time_rule(ScheduleTimeRule::physical_time(15, 0));
|
||||
|
||||
assert!(scheduler.is_due_on(d(2025, 1, 30), &daily));
|
||||
assert!(scheduler.is_due_on(d(2025, 1, 31), &daily));
|
||||
assert!(
|
||||
scheduler
|
||||
.triggered_rules_at(
|
||||
d(2025, 1, 30),
|
||||
ScheduleStage::OnDay,
|
||||
Some(NaiveTime::from_hms_opt(15, 0, 0).unwrap()),
|
||||
std::slice::from_ref(&daily),
|
||||
)
|
||||
.len()
|
||||
== 1
|
||||
);
|
||||
assert!(
|
||||
scheduler
|
||||
.triggered_rules_at(
|
||||
d(2025, 1, 30),
|
||||
ScheduleStage::OnDay,
|
||||
Some(NaiveTime::from_hms_opt(10, 18, 0).unwrap()),
|
||||
std::slice::from_ref(&daily),
|
||||
)
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scheduler_matches_daily_weekly_and_monthly_rules() {
|
||||
let calendar = sample_calendar();
|
||||
|
||||
+865
-515
File diff suppressed because it is too large
Load Diff
@@ -69,7 +69,30 @@ pub struct StrategyAiCatalog {
|
||||
pub indicator_factors: Vec<String>,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub clickhouse_table_fields: Vec<ManualFactorSource>,
|
||||
pub data_lake_fields: Vec<ManualFactorSource>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StrategyAiHoldingCountContract {
|
||||
#[serde(
|
||||
default,
|
||||
alias = "holdingCount",
|
||||
alias = "holding_count",
|
||||
alias = "targetHoldingCount",
|
||||
alias = "target_holding_count"
|
||||
)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub count: Option<i64>,
|
||||
#[serde(
|
||||
default,
|
||||
alias = "kind",
|
||||
alias = "holdingCountMode",
|
||||
alias = "holding_count_mode",
|
||||
alias = "targetHoldingCountMode",
|
||||
alias = "target_holding_count_mode"
|
||||
)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mode: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -79,6 +102,9 @@ pub struct StrategyAiGenerateRequest {
|
||||
pub market: String,
|
||||
pub benchmark_symbol: String,
|
||||
pub signal_symbol: String,
|
||||
#[serde(default, alias = "holdingCountContract")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub holding_count_contract: Option<StrategyAiHoldingCountContract>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -87,8 +113,15 @@ pub struct StrategyAiOptimizeRequest {
|
||||
pub objective: String,
|
||||
pub result_summary: serde_json::Value,
|
||||
pub diagnostics: Vec<String>,
|
||||
#[serde(default, alias = "holdingCountContract")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub holding_count_contract: Option<StrategyAiHoldingCountContract>,
|
||||
}
|
||||
|
||||
const PERFORMANCE_ACCEPTANCE_CONTRACT_PROMPT: &str = "收益验收合同:收益、回撤、年度收益、样本外区间及比较运算符只能来自用户目标、请求约束或不可变 candidate/promotion contract;不得注入 120% 或其他默认数值,也不得提高、降低或替换已经明确的门槛。没有明确数值合同时只做策略有效性、数据时序和风险审计,禁止声称收益已经达标;存在冻结合同时必须逐项按原运算符验证,不能只看总收益。";
|
||||
const DEFAULT_RISK_POLICY_DSL_PROMPT: &str = "max_order_quantity=1000000、max_order_notional=100000000、max_symbol_position=10000000、reject_st_selection=false、reject_st_buy=true、reject_star_st_selection=false、reject_star_st_buy=true、reject_paused_selection=false、reject_paused_buy=true、reject_paused_sell=true、reject_inactive_selection=false、reject_inactive_buy=true、reject_inactive_sell=true、reject_new_listing_selection=false、reject_new_listing_buy=true、reject_kcb_selection=false、reject_kcb_buy=true、reject_bjse_selection=false、reject_bjse_buy=true、reject_one_yuan_selection=false、reject_one_yuan_buy=true、respect_allow_buy_sell=true、reject_upper_limit_selection=false、reject_lower_limit_selection=false、reject_upper_limit_buy=true、reject_lower_limit_sell=true、forbid_same_day_rebuy_after_sell=true、blacklist_enabled=true、allow_market_orders=true、live_trading_enabled=false、volume_limit_enabled=true、liquidity_limit_enabled=true、volume_percent=0.25、commission_rate=0.0003、minimum_commission=5、stamp_tax_rate_before_change=0.001、stamp_tax_rate_after_change=0.0005、stamp_tax_change_date=\"2023-08-28\"";
|
||||
const DEFAULT_RISK_POLICY_DSL_CODE: &str = "max_order_quantity=1000000, max_order_notional=100000000, max_symbol_position=10000000, reject_st_selection=false, reject_st_buy=true, reject_star_st_selection=false, reject_star_st_buy=true, reject_paused_selection=false, reject_paused_buy=true, reject_paused_sell=true, reject_inactive_selection=false, reject_inactive_buy=true, reject_inactive_sell=true, reject_new_listing_selection=false, reject_new_listing_buy=true, reject_kcb_selection=false, reject_kcb_buy=true, reject_bjse_selection=false, reject_bjse_buy=true, reject_one_yuan_selection=false, reject_one_yuan_buy=true, respect_allow_buy_sell=true, reject_upper_limit_selection=false, reject_lower_limit_selection=false, reject_upper_limit_buy=true, reject_lower_limit_sell=true, forbid_same_day_rebuy_after_sell=true, blacklist_enabled=true, allow_market_orders=true, live_trading_enabled=false, volume_limit_enabled=true, liquidity_limit_enabled=true, volume_percent=0.25, commission_rate=0.0003, minimum_commission=5, stamp_tax_rate_before_change=0.001, stamp_tax_rate_after_change=0.0005, stamp_tax_change_date=\"2023-08-28\"";
|
||||
|
||||
pub fn built_in_strategy_manual() -> StrategyAiManual {
|
||||
StrategyAiManual {
|
||||
title: "OmniQuant 平台策略脚本手册".to_string(),
|
||||
@@ -97,11 +130,12 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
||||
"平台策略脚本采用声明式 DSL + 表达式执行模型。".to_string(),
|
||||
"支持 let 变量、fn 自定义函数、when/unless/else 条件块、可用指标/因子字段映射。".to_string(),
|
||||
"支持数值型和字符串型因子,字符串字段可用于行业、概念、标签、板块等分类过滤。".to_string(),
|
||||
"当前默认回测数据已支持 OHLCV、市值、流通市值、换手率、有效换手率、上市天数、停牌/ST/板块、涨跌停价格、tick 触达涨跌停、常用价格/成交量均线,以及 stock_indicator_factors_v1 中已入库的通用指标因子。".to_string(),
|
||||
"用户明确指定目标持仓数量或最低持仓数量时,selection.limit 必须严格表达该数量;不要因为优化收益、减少交易或转换框架而擅自改小持仓数。".to_string(),
|
||||
"当前默认回测数据已支持 OHLCV、市值、流通市值、换手率、有效换手率、上市天数、停牌/ST/板块、涨跌停价格、分钟线触达涨跌停、常用价格/成交量均线,以及 stock_indicator_factors_v1 中已入库的通用指标因子。".to_string(),
|
||||
"AI 生成策略时只能输出完整 engine-script 代码,不输出 Markdown、解释、推理过程、JSON 包装或手册复述。".to_string(),
|
||||
"表达式字段以运行时字段为准:市值使用 market_cap,流通市值使用 free_float_cap;不要在策略表达式中使用数据库原始字段 float_market_cap。".to_string(),
|
||||
"任意窗口价格均线使用 rolling_mean(\"close\", n) 或 ma(\"close\", n),任意窗口均量使用 rolling_mean(\"volume\", n) 或 vma(n);不要使用未列出的 ma60、stock_ma60、signal_ma60 或 benchmark_ma60 变量。".to_string(),
|
||||
"next_bar_open 会用决策日信号生成订单,并在下一可交易开盘撮合;不得把执行日 open/high/low/close 当成下单前已知信息。".to_string(),
|
||||
"next_bar_open 会在 T 日收盘冻结目标金额或目标权益,并在下一可交易日按实际 open、滑点、手续费和证券数量步长重算股数;不得把执行日 open/high/low/close 当成下单前已知信息,也不得用 T+1 prev_close 或 T 日估算股数直接成交;涨停买入和跌停卖出风控必须用实际 next-open 成交价比较,不能用执行日 close/last 或 next-close。".to_string(),
|
||||
"自定义 fn 必须通过参数传入运行时字段;不要用 fn score() 这类零参数函数直接引用 market_cap、close、ma5 等股票字段。".to_string(),
|
||||
"禁止自由 Python/JavaScript 命令式语句,最终必须输出平台 DSL。".to_string(),
|
||||
],
|
||||
@@ -203,8 +237,8 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
||||
detail: "支持按交易周或交易月调仓,例如 rebalance.weekly(weekday=5).at([\"10:18\"])、rebalance.weekly(tradingday=-1).at([\"10:18\"])、rebalance.monthly(tradingday=1).at([\"10:18\"])。`.at([...])` 的最后一个时刻会编进分钟级 schedule/time_rule;当前平台把 on_day 近似到 10:18,把 open_auction 近似到 09:31。".to_string(),
|
||||
},
|
||||
ManualSection {
|
||||
title: "bar / tick 生命周期".to_string(),
|
||||
detail: "回测内核支持 平台内核 风格的 bar/tick 生命周期:日内会发布 pre_bar/bar/post_bar 过程事件;存在 tick 订阅或 tick 调度规则时,会按 execution_quotes 的时间顺序发布 pre_tick/tick/post_tick,并把 tick 阶段下单限制在当前 tick 时间窗内撮合。平台 DSL 中可通过 subscribe([...])、trading.subscription_guard(true) 和 process_event 字段配合显式订单模拟 tick 订阅策略。".to_string(),
|
||||
title: "bar / minute execution 生命周期".to_string(),
|
||||
detail: "回测内核支持 平台内核 风格的 bar/分钟执行价生命周期:日内会发布 pre_bar/bar/post_bar 过程事件;存在分钟执行价订阅或分钟调度规则时,会按 execution_quotes 的时间顺序发布 pre_minute/minute/post_minute 过程事件,并把日内阶段下单限制在当前分钟执行价时间窗内撮合。平台 DSL 中可通过 subscribe([...])、trading.subscription_guard(true) 和 process_event 字段配合显式订单模拟日内订阅策略。".to_string(),
|
||||
},
|
||||
ManualSection {
|
||||
title: "selection.market_cap_band / selection.limit / ordering.rank_by / ordering.rank_expr".to_string(),
|
||||
@@ -214,21 +248,33 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
||||
title: "filter.stock_expr / risk.stop_loss / risk.take_profit / allocation.buy_scale".to_string(),
|
||||
detail: "表达式型规则,支持多条组合。stop_loss/take_profit 多条按 OR 组合,filter.stock_expr 多条按 AND 组合。".to_string(),
|
||||
},
|
||||
ManualSection {
|
||||
title: "risk.policy / risk.blacklist".to_string(),
|
||||
detail: "统一配置 FIDC 基础风控。risk.policy(...) 支持 max_order_quantity、max_order_notional、max_symbol_position,以及 ST/*ST、停牌、退市、新股、科创、北交所、一元、涨跌停、同日卖出禁买、黑名单、成交量、流动性和交易成本等命名参数;risk.blacklist([\"600000.SH\"]) 写策略级黑名单。框架默认基础风控必须走 risk.policy 或运行态 RiskLimits,不能被转换器隐式写进 universe.exclude 或 filter.stock_expr;源策略明确写出的业务选股排除属于策略本身,必须原样保留,不能反向修改冻结的 reject_*_selection 开关;冻结的 `reject_*_selection` 值不得改变。PG/Source Lake 是真相源,Redis 只可做当日锁、热配置缓存和配置变更通知。".to_string(),
|
||||
},
|
||||
ManualSection {
|
||||
title: "corporate_actions.dividend_reinvestment".to_string(),
|
||||
detail: "支持 corporate_actions.dividend_reinvestment(true)。开启后,现金分红到账会优先按 round lot 回补成同一只股票,零头保留为现金。".to_string(),
|
||||
},
|
||||
ManualSection {
|
||||
title: "execution.matching_type / execution.slippage".to_string(),
|
||||
detail: "设置撮合模式和滑点。支持 execution.matching_type(\"next_tick_last\" | \"next_tick_best_own\" | \"next_tick_best_counterparty\" | \"counterparty_offer\" | \"vwap\" | \"current_bar_close\" | \"next_bar_open\" | \"open_auction\")。其中 next_tick_last 使用 tick 的 last_price;next_tick_best_own / next_tick_best_counterparty 会按 L1 买一卖一近似 平台内核 的 tick 最优价语义;counterparty_offer 在存在 order_book_depth 多档盘口数据时会按真实档位逐档扫单并计算加权成交价,不存在 depth 时回退 L1 对手方报价;vwap 会在盘中执行价链路上聚合多笔成交为单条 VWAP 成交;next_bar_open 使用决策日信号并在下一可交易日开盘撮合,禁止把执行日 open/high/low/close 解释为下单前已知数据;open_auction 使用当日集合竞价开盘价 day_open 进行撮合,且不额外施加滑点,并按竞价成交量而不是盘口一档流动性限制成交;滑点支持 execution.slippage(\"none\") / execution.slippage(\"price_ratio\", 0.001) / execution.slippage(\"tick_size\", 1) / execution.slippage(\"limit_price\"),其中 limit_price 会在限价单成交时按挂单价模拟 平台内核 的最坏成交价。".to_string(),
|
||||
detail: "设置回测全局撮合模式和滑点。日线回测只允许 execution.matching_type(\"current_bar_close\") 或 execution.matching_type(\"next_bar_open\");current_bar_close 使用决策日当日 close,next_bar_open 在 T 日收盘冻结目标金额或目标权益,并在下一可交易日按实际 open、滑点、手续费和证券数量步长重算股数,保证执行金额加手续费不超过分配金额;禁止把执行日 open/high/low/close 解释为下单前已知数据,也禁止用 T+1 prev_close 或 T 日估算股数直接成交;next_bar_open 的涨停买入和跌停卖出判断必须比较实际 open 成交价与涨跌停价,不能用执行日 close/last 或 next-close。金额预算始终严格,execution.strict_value_budget(false) 会被拒绝。分钟线回测使用当前分钟价格成交,只能写 execution.matching_type(\"minute_last\");不要把 vwap、twap、open_auction、minute_best_own、minute_best_counterparty 写成全局 matching_type,这些只属于显式订单或内部撮合能力。日线调仓现金口径由 execution.rebalance_cash_mode(\"sell_then_buy\" | \"same_point_net\" | \"pre_open_cash\") 或页面/API 参数控制,默认 sell_then_buy;sell_then_buy_delay_slippage_rate 只来自页面/API 执行参数,默认 0,不要写进策略表达式。滑点支持 execution.slippage(\"none\") / execution.slippage(\"price_ratio\", 请求滑点率,例如 0.002) / execution.slippage(\"tick_size\", 1) / execution.slippage(\"limit_price\"),其中 limit_price 会在限价单成交时按挂单价模拟 平台内核 的最坏成交价。".to_string(),
|
||||
},
|
||||
ManualSection {
|
||||
title: "期货提交校验".to_string(),
|
||||
detail: "期货订单进入撮合前会先执行账户与交易规则校验:合约必须在上市/退市日期范围内,日行情不能停牌,trading_phase 需处于 continuous/trading/open_auction/auction/call_auction/opening_auction 等可交易阶段,限价必须为正且按 futures_trading_parameters.price_tick 或日行情 price_tick 对齐,并且不能越过 upper_limit/lower_limit;随后继续检查反向挂单自成交风险、保证金和可平数量。服务层可通过 FuturesValidationConfig 分别关闭 active instrument、trading phase、limit price tick、price limit 校验,用于兼容特殊数据,但默认全部开启。".to_string(),
|
||||
title: "期货 runtime action 与提交校验".to_string(),
|
||||
detail: "runtimeExpressions.trading.actions 支持 futures_order、futures_open、futures_close、futures_close_today、futures_close_yesterday;字段包括 symbol、direction=long|short、quantityExpr/amountExpr、可选 limitPriceExpr、transactionCostExpr、whenExpr 和 reason。期货-only 策略把请求初始资金分配给期货账户且股票账户为0;股票+期货混合策略必须显式声明 futuresInitialCash,可选 stockInitialCash。合约必须先由 Source Lake 发布 futures_contract_daily、futures_contract_spec_history、futures_cost_margin_history 三张真实数据集;缺任一张时生成/回测必须失败,禁止手写默认乘数、保证金、费用或价格。订单进入撮合前继续检查上市/退市日期、停牌、trading_phase、限价 tick、涨跌停、反向挂单自成交、保证金和可平今昨仓。".to_string(),
|
||||
},
|
||||
ManualSection {
|
||||
title: "trading.rotation / order.* / cancel.* / update_universe / subscribe".to_string(),
|
||||
detail: "支持显式下单、撤单、AlgoOrder、动态 universe 和账户资金动作。可以用 trading.rotation(false) 关闭默认轮动链路,再用 trading.stage(\"open_auction\" | \"on_day\") 指定执行阶段;需要模拟 平台内核 的 tick 订阅保护时,可写 trading.subscription_guard(true),未订阅 symbol 的显式订单会被拦截,TargetPortfolioSmart + AlgoOrder 会过滤未订阅标的。用 trading.schedule.daily().at([\"10:18\"]) / trading.schedule.weekly(weekday=5).at([\"10:18\"]) / trading.schedule.weekly(tradingday=-1).at([\"10:18\"]) / trading.schedule.monthly(tradingday=1).at([\"10:18\"]) 指定触发频率和分钟级 time_rule,然后写 order.shares(\"600000.SH\", 1000)、order.target_shares(\"600000.SH\", 2000)、order.value(\"600000.SH\", cash * 0.25)、order.target_percent(\"600000.SH\", 0.05)、order.limit_value(\"600000.SH\", cash * 0.25, open * 0.99)、order.vwap_value(\"600000.SH\", cash * 0.25, \"09:31\", \"09:40\")、order.twap_percent(\"600000.SH\", 0.05, \"10:00\", \"10:30\")、order.target_portfolio_smart(weights={\"600000.SH\": 0.3, \"000001.SZ\": 0.2}, order_prices=VWAPOrder(930, 940), valuation_prices={\"600000.SH\": prev_close})、order.target_portfolio_smart(weights={\"600000.SH\": 0.3, \"000001.SZ\": 0.2}, order_prices={\"600000.SH\": open * 0.99}, valuation_prices={\"600000.SH\": prev_close})、cancel.order(12345)、cancel.symbol(\"600000.SH\")、cancel.all()、update_universe([\"600000.SH\", \"000001.SZ\"])、subscribe([\"000001.SZ\"])、unsubscribe([\"000001.SZ\"])、account.deposit_withdraw(100000, receiving_days=0)、account.finance_repay(50000)、account.set_management_fee_rate(0.001)。其中 order.target_shares(...) 对应 平台内核 的 order_to,order.target_portfolio_smart(...) 对应 平台内核 的 order_target_portfolio_smart 批量目标权重语义;account.deposit_withdraw(...) 和 account.finance_repay(...) 对应 平台内核 账户出入金与融资/还款语义;order_prices 既可以是逐标的限价映射,也可以是 VWAPOrder/TWAPOrder 这类全局 AlgoOrder;order.vwap_* / order.twap_* 对应 平台内核 的 AlgoOrder 时间窗订单风格,而 update_universe/subscribe/unsubscribe 对应 平台内核 的动态 universe 与订阅接口。symbol 使用标准证券代码;数量、金额、仓位、时间窗、限价、order_id 和 symbol 列表都支持表达式;这些语句也支持放进 when/unless 条件块。".to_string(),
|
||||
title: "trading.rotation / order.* / order.modify / cancel.* / update_universe / subscribe".to_string(),
|
||||
detail: "支持股票显式下单、期货 runtime action、撤单、AlgoOrder、动态 universe 和账户资金动作。可以用 trading.rotation(false) 关闭默认轮动链路,再用 trading.stage(\"open_auction\" | \"on_day\") 指定执行阶段;需要模拟 平台内核 的日内订阅保护时,可写 trading.subscription_guard(true),未订阅 symbol 的显式订单会被拦截,TargetPortfolioSmart + AlgoOrder 会过滤未订阅标的。用 trading.schedule.daily().at([\"10:18\"]) / trading.schedule.weekly(weekday=5).at([\"10:18\"]) / trading.schedule.weekly(tradingday=-1).at([\"10:18\"]) / trading.schedule.monthly(tradingday=1).at([\"10:18\"]) 指定触发频率和分钟级 time_rule,然后写 order.shares(\"600000.SH\", 1000)、order.target_shares(\"600000.SH\", 2000)、order.value(\"600000.SH\", cash * 0.25)、order.target_percent(\"600000.SH\", 0.05)、order.limit_value(\"600000.SH\", cash * 0.25, open * 0.99, time_in_force=\"gtc\")、order.vwap_value(\"600000.SH\", cash * 0.25, \"09:31\", \"09:40\")、order.twap_percent(\"600000.SH\", 0.05, \"10:00\", \"10:30\")、order.target_portfolio_smart(weights={\"600000.SH\": 0.3, \"000001.SZ\": 0.2}, order_prices=VWAPOrder(930, 940), valuation_prices={\"600000.SH\": prev_close})、cancel.order(12345)、cancel.symbol(\"600000.SH\")、cancel.all()、update_universe([\"600000.SH\", \"000001.SZ\"])、subscribe([\"000001.SZ\"])、unsubscribe([\"000001.SZ\"])、account.deposit_withdraw(100000, receiving_days=0)、account.finance_repay(50000)、account.set_management_fee_rate(0.001)。股票订单和 target_portfolio_smart 支持可选关键字 time_in_force=\"day|ioc|fok|gtc\",编译后写入 runtimeExpressions.trading.actions[].timeInForce:DAY 日内保留并在收盘 Expired,IOC 立即撤销未成交余量,FOK 必须全量可成交否则零成交,GTC 仅支持限价单并跨交易日保留;VWAP/TWAP 不接受 FOK/GTC。期货 action 必须由编译器写入结构化 runtimeExpressions,不得让策略源码直接构造 FuturesOrderIntent 或硬编码合约参数。symbol 使用标准证券/合约代码;数量、金额、仓位、时间窗、限价、order_id 和 symbol 列表都支持表达式;这些语句也支持放进 when/unless 条件块。".to_string(),
|
||||
},
|
||||
ManualSection {
|
||||
title: "order.time_in_force target runtime scope".to_string(),
|
||||
detail: "回测支持 DAY/IOC/FOK/GTC;paper/live 当前只支持 DAY/IOC/FOK。GTC 需要持久化跨交易日 parent/child 重挂账本和券商适配器能力,在该合同实现前只允许回测,paper/live 必须明确拒绝并禁止降级为 DAY。生成策略前必须按目标运行模式选择能力。".to_string(),
|
||||
},
|
||||
ManualSection {
|
||||
title: "order.modify".to_string(),
|
||||
detail: "回测中可用 order.modify(order_id, total_quantity=?, limit_price=?) 原位修改仍未完成的限价单。total_quantity 是新的总委托量而不是增量,不能低于已成交量;改价或增量会重置盘口队列优先级,减少总量且不改价保留优先级,同时保留 order_id、有效期、累计成交和费用状态。paper/live 在适配器提供持久且确认的 cancel-replace 合同前必须拒绝该动作,不得静默转换为撤单加新订单。".to_string(),
|
||||
},
|
||||
ManualSection {
|
||||
title: "when / unless / else".to_string(),
|
||||
@@ -242,6 +288,7 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
||||
fields: vec![
|
||||
ManualField { name: "signal_open/signal_close".to_string(), field_type: "float".to_string(), detail: "信号指数当日开盘价与前一日收盘价。".to_string() },
|
||||
ManualField { name: "benchmark_open/benchmark_close".to_string(), field_type: "float".to_string(), detail: "基准当日开盘价与前一日收盘价。".to_string() },
|
||||
ManualField { name: "benchmark_signal_close".to_string(), field_type: "float".to_string(), detail: "信号日收盘后可见的基准当日收盘价;用于 T 日生成信号、T+1 开盘成交的条件,不能在盘中或 T 日开盘决策中使用。".to_string() },
|
||||
ManualField { name: "signal_ma5/signal_ma10/signal_ma20/signal_ma30".to_string(), field_type: "float".to_string(), detail: "信号指数滚动均线。".to_string() },
|
||||
ManualField { name: "benchmark_ma5/benchmark_ma10/benchmark_ma20/benchmark_ma30".to_string(), field_type: "float".to_string(), detail: "基准指数滚动均线。".to_string() },
|
||||
ManualField { name: "cash/available_cash/frozen_cash/market_value/total_equity".to_string(), field_type: "float".to_string(), detail: "账户可用资金、挂单冻结资金、市值与总权益;available_cash 会扣减当前买入挂单冻结估算。".to_string() },
|
||||
@@ -267,17 +314,17 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
||||
fields: vec![
|
||||
ManualField { name: "symbol".to_string(), field_type: "string".to_string(), detail: "证券代码。".to_string() },
|
||||
ManualField { name: "market_cap/free_float_cap".to_string(), field_type: "float".to_string(), detail: "总市值、流通市值。".to_string() },
|
||||
ManualField { name: "turnover/turnover_ratio/effective_turnover_ratio".to_string(), field_type: "float".to_string(), detail: "换手率、换手率标准字段、有效换手率;turnover 是 turnover_ratio 的兼容别名。".to_string() },
|
||||
ManualField { name: "turnover_ratio/effective_turnover_ratio".to_string(), field_type: "float".to_string(), detail: "换手率标准字段和有效换手率。".to_string() },
|
||||
ManualField { name: "open/high/low/close/last/last_price/prev_close/amount".to_string(), field_type: "float".to_string(), detail: "开盘、最高、最低、收盘、盘中价、昨收和成交额。".to_string() },
|
||||
ManualField { name: "upper_limit/lower_limit/price_tick/round_lot/minimum_order_quantity/order_step_size".to_string(), field_type: "float/int".to_string(), detail: "涨跌停、最小价位、整手、最小下单量和数量步长。KSH/BJSE 等板块可与 round_lot 不同。".to_string() },
|
||||
ManualField { name: "paused/is_st/is_kcb/is_one_yuan/is_new_listing".to_string(), field_type: "bool".to_string(), detail: "可交易性与板块标志。".to_string() },
|
||||
ManualField { name: "paused/is_st/is_star_st/is_kcb/is_one_yuan/is_new_listing".to_string(), field_type: "bool".to_string(), detail: "可交易性与板块标志,ST 与 *ST 是独立字段。".to_string() },
|
||||
ManualField { name: "allow_buy/allow_sell/at_upper_limit/at_lower_limit".to_string(), field_type: "bool".to_string(), detail: "盘中买卖与涨跌停状态。".to_string() },
|
||||
ManualField { name: "touched_upper_limit/touched_lower_limit/hit_upper_limit/hit_lower_limit".to_string(), field_type: "bool".to_string(), detail: "当日 tick 曾经触达涨跌停。".to_string() },
|
||||
ManualField { name: "touched_upper_limit/touched_lower_limit/hit_upper_limit/hit_lower_limit".to_string(), field_type: "bool".to_string(), detail: "当日分钟执行价曾经触达涨跌停。".to_string() },
|
||||
ManualField { name: "symbol_open_order_count/symbol_open_buy_qty/symbol_open_sell_qty/latest_symbol_open_order_id".to_string(), field_type: "int".to_string(), detail: "当前证券在挂单簿中的未成交挂单摘要和最近挂单 id。".to_string() },
|
||||
ManualField { name: "latest_symbol_open_order_status/latest_symbol_open_order_unfilled_qty".to_string(), field_type: "string/int".to_string(), detail: "当前证券最近一笔挂单的状态和未成交数量。".to_string() },
|
||||
ManualField { name: "in_dynamic_universe/is_subscribed".to_string(), field_type: "bool".to_string(), detail: "当前证券是否在动态 universe 内,以及是否仍在订阅集合中。".to_string() },
|
||||
ManualField { name: "stock_ma5/stock_ma10/stock_ma20/stock_ma30".to_string(), field_type: "float".to_string(), detail: "个股价格均线内建别名,按当前交易日前 N 个已完成交易日的收盘价计算;历史窗口不足时为 NaN,比较条件会自然不通过;15 日、45 日等任意窗口请改用 sma(\"close\", n)。".to_string() },
|
||||
ManualField { name: "stock_volume_ma5/stock_volume_ma10/stock_volume_ma20/stock_volume_ma60".to_string(), field_type: "float".to_string(), detail: "个股成交量均线内建别名,按当前交易日前 N 个已完成交易日的成交量计算,不包含回测当天未来成交量;历史窗口不足时为 NaN,比较条件会自然不通过;任意窗口请改用 rolling_mean(\"volume\", n)。".to_string() },
|
||||
ManualField { name: "stock_volume_ma5/stock_volume_ma10/stock_volume_ma20/stock_volume_ma60/stock_volume_ma100".to_string(), field_type: "float".to_string(), detail: "个股成交量均线内建别名,按当前交易日前 N 个已完成交易日的成交量计算,不包含回测当天未来成交量;历史窗口不足时为 NaN,比较条件会自然不通过;任意窗口请改用 rolling_mean(\"volume\", n)。".to_string() },
|
||||
ManualField { name: "factors[\"field\"] / factor(\"field\")".to_string(), field_type: "float/string".to_string(), detail: "当前证券当日可用因子。默认可用字段以手册的“可用指标、参数和字段”清单为准;自定义因子需要预先写入策略数据或 extra_factors。数值字段返回数字,字符串字段返回字符串。".to_string() },
|
||||
ManualField { name: "listed_days".to_string(), field_type: "int".to_string(), detail: "上市天数。".to_string() },
|
||||
],
|
||||
@@ -304,13 +351,13 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
||||
functions: vec![
|
||||
ManualFunction { name: "factor".to_string(), signature: "factor(\"column_name\")".to_string(), detail: "读取当前股票当日可用因子列。数值因子返回 float,字符串因子返回 string;缺失字段默认返回 0 或空字符串,建议重要条件配合 diagnostics 查看候选过滤数量。".to_string() },
|
||||
ManualFunction { name: "day_factor".to_string(), signature: "day_factor(\"field_name\")".to_string(), detail: "读取日级/指数级字段映射。".to_string() },
|
||||
ManualFunction { name: "history_bars".to_string(), signature: "ctx.history_bars(symbol, count, \"1d\" | \"1m\" | \"tick\", \"close\", include_now)".to_string(), detail: "回测内核策略上下文数据 API,返回指定证券最近 N 条数值序列。日线字段支持 open/high/low/close/last/prev_close/volume/upper_limit/lower_limit;分钟或 tick 字段支持 last/bid1/ask1/volume_delta/amount_delta。日线 include_now=false 排除当前交易日;分钟/tick 会按当前 on_bar、on_tick 或调度时刻截断,include_now=false 排除当前 bar/tick,避免未来函数。".to_string() },
|
||||
ManualFunction { name: "current_snapshot".to_string(), signature: "ctx.current_snapshot(symbol)".to_string(), detail: "读取当前交易日指定证券的日级快照,可用于获得当日 open/close/last/upper_limit/lower_limit 等字段。".to_string() },
|
||||
ManualFunction { name: "history_bars".to_string(), signature: "ctx.history_bars(symbol, count, \"1d\" | \"1m\", \"close\", include_now)".to_string(), detail: "回测内核策略上下文数据 API,返回指定证券最近 N 条数值序列。日线字段支持 open/high/low/close/last/prev_close/volume/upper_limit/lower_limit;分钟字段支持 last/bid1/ask1/volume_delta/amount_delta。日线 include_now=false 排除当前信号日;分钟线会按当前 on_bar、日内事件或调度时刻截断,include_now=false 排除当前分钟执行价。next_bar_open 下该 API 只能看到信号日数据,不能读取实际成交日数据。".to_string() },
|
||||
ManualFunction { name: "current_snapshot".to_string(), signature: "ctx.current_snapshot(symbol)".to_string(), detail: "读取当前信号日指定证券的日级快照,可用于获得信号日 open/close/last/upper_limit/lower_limit 等字段;next_bar_open 的实际成交日涨跌停、停牌、ST、退市、一元、黑名单、成交量和盘口流动性由撮合层按执行日判断。".to_string() },
|
||||
ManualFunction { name: "instrument/instruments/all_instruments".to_string(), signature: "ctx.instrument(symbol)".to_string(), detail: "读取证券元数据,包括名称、板块、上市日期、退市日期、最小下单量、整手、最小价位等;all_instruments 按证券代码稳定排序返回全量证券。".to_string() },
|
||||
ManualFunction { name: "active_instruments/instruments_history".to_string(), signature: "ctx.active_instruments(&[symbol])".to_string(), detail: "active_instruments 返回当前交易日已上市且未退市的证券;instruments_history 返回给定代码的历史证券记录,包含当前已退市标的,对齐 平台内核 的 active_instruments/instruments_history 能力。".to_string() },
|
||||
ManualFunction { name: "active_instruments/instruments_history".to_string(), signature: "ctx.active_instruments(&[symbol])".to_string(), detail: "active_instruments 返回当前信号日已上市且未退市的证券;instruments_history 返回给定代码的历史证券记录,包含当前已退市标的,对齐 平台内核 的 active_instruments/instruments_history 能力。".to_string() },
|
||||
ManualFunction { name: "get_trading_dates/get_previous_trading_date/get_next_trading_date".to_string(), signature: "ctx.get_previous_trading_date(date, n)".to_string(), detail: "交易日历 API。get_trading_dates 返回闭区间交易日;previous/next 返回相对某日向前或向后的第 n 个交易日,当前日自身不计入。".to_string() },
|
||||
ManualFunction { name: "is_suspended/is_st_stock".to_string(), signature: "ctx.is_suspended(symbol, count)".to_string(), detail: "读取指定证券截至当前交易日最近 count 个交易日的停牌或 ST 标记,返回 bool 序列,顺序从旧到新;对应平台内核的 is_suspended/is_st_stock 数据能力。".to_string() },
|
||||
ManualFunction { name: "get_price".to_string(), signature: "ctx.get_price(symbol, start_date, end_date, \"1d\" | \"1m\" | \"tick\")".to_string(), detail: "按日期区间读取统一 PriceBar 序列。日线返回 open/high/low/close/last/volume/盘口字段;分钟或 tick 返回按 timestamp 排序的 last/bid1/ask1/volume_delta/amount_delta 映射,便于服务层转成表格或前端明细。".to_string() },
|
||||
ManualFunction { name: "is_suspended/is_st_stock".to_string(), signature: "ctx.is_suspended(symbol, count)".to_string(), detail: "读取指定证券截至当前信号日最近 count 个交易日的停牌或 ST 标记,返回 bool 序列,顺序从旧到新;对应平台内核的 is_suspended/is_st_stock 数据能力。执行日停牌或 ST 只能由撮合风控判断,不能在 next_bar_open 的 T 日提前固化。".to_string() },
|
||||
ManualFunction { name: "get_price".to_string(), signature: "ctx.get_price(symbol, start_date, end_date, \"1d\" | \"1m\")".to_string(), detail: "按日期区间读取统一 PriceBar 序列。日线返回 open/high/low/close/last/volume/盘口字段;分钟线返回按 timestamp 排序的 last/bid1/ask1/volume_delta/amount_delta 映射,便于服务层转成表格或前端明细。".to_string() },
|
||||
ManualFunction { name: "get_dividend / dividend_cash / has_dividend".to_string(), signature: "dividend_cash(lookback) / has_dividend(lookback)".to_string(), detail: "高级数据 风格分红 API。Rust Context 可用 ctx.get_dividend(symbol, start_date) 读取明细;平台表达式可用 dividend_cash(lookback) 汇总当前股票最近 N 个交易日现金分红,用 has_dividend(lookback) 判断是否发生分红,也支持 dividend_cash(\"600000.SH\", lookback)。".to_string() },
|
||||
ManualFunction { name: "get_split / split_ratio / has_split".to_string(), signature: "split_ratio(lookback) / has_split(lookback)".to_string(), detail: "高级数据 风格拆分/送转 API。Rust Context 可用 ctx.get_split(symbol, start_date) 读取明细;平台表达式可用 split_ratio(lookback) 计算当前股票最近 N 个交易日累计拆分比例,has_split(lookback) 判断是否发生送转。".to_string() },
|
||||
ManualFunction { name: "get_factor / factor_value".to_string(), signature: "factor_value(\"field\", lookback=1)".to_string(), detail: "数值因子 API。factor(\"field\") 读取当前股票当日因子;factor_value(\"field\", lookback) 会在最近 N 个交易日内取该字段最新数值,适合读取任意可用指标或自定义数值因子。Rust Context 可用 ctx.get_factor(symbol, start, end, field) 读取完整数值序列。".to_string() },
|
||||
@@ -327,14 +374,14 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
||||
ManualFunction { name: "get_dominant_future / dominant_future / dominant_future_price".to_string(), signature: "dominant_future(\"IF\") / dominant_future_price(\"IF\", \"close\", lookback=1)".to_string(), detail: "主力合约 API。dominant_future 返回当前日期匹配前缀的主力期货合约代码;dominant_future_price 读取该主力合约最近 N 个交易日指定字段的最新价格。Rust Context 可用 ctx.get_dominant_future(...) 和 ctx.get_dominant_future_price(...)。".to_string() },
|
||||
ManualFunction { name: "order/order_status/order_avg_price/order_transaction_cost".to_string(), signature: "ctx.order(order_id)".to_string(), detail: "按订单 id 查询运行时订单对象,支持已结束订单和当前挂单。返回字段包括 status、filled_quantity、unfilled_quantity、avg_price、transaction_cost、symbol、side、reason;可用便捷函数读取状态、成交均价和费用,对齐 平台内核 Order 的核心属性。".to_string() },
|
||||
ManualFunction { name: "account/portfolio_view/accounts".to_string(), signature: "ctx.account()".to_string(), detail: "返回当前股票账户/组合运行时视图,字段包括 account_type、cash、available_cash、frozen_cash、market_value、total_value、unit_net_value、daily_pnl、daily_returns、total_returns、transaction_cost、trading_pnl、position_pnl 等;DSL 中同名字段可直接使用。也可用 ctx.stock_account()、ctx.account_by_type(\"STOCK\")、ctx.accounts() 按账户类型读取;当前股票回测路径不会把 FUTURE 虚假映射成 STOCK。".to_string() },
|
||||
ManualFunction { name: "deposit_withdraw/finance_repay/management_fee".to_string(), signature: "account.deposit_withdraw(amount, receiving_days=0)".to_string(), detail: "策略账户资金动作。deposit_withdraw 正数入金、负数出金;receiving_days 大于 0 时按交易日延迟到账,并保持净值口径不把外部资金流当成收益。finance_repay 正数融资、负数还款,会同步维护 cash_liabilities。set_management_fee_rate 设置结算管理费率;普通策略可覆盖 management_fee(ctx, rate) 自定义计算器,对齐 平台内核 管理费回调能力。".to_string() },
|
||||
ManualFunction { name: "rolling_mean / sma / ma".to_string(), signature: "rolling_mean(\"field\", lookback) / ma(\"close\", 20)".to_string(), detail: "任意字段滚动均值,支持 close、volume、amount、turnover_ratio、effective_turnover_ratio、signal_open/signal_close、benchmark_open/benchmark_close 和所有数值型 extra_factors。个股 close 使用当前交易日前已完成收盘序列,volume 使用当前交易日前已完成成交量序列;历史窗口不足时在选股过滤和买入仓位表达式中按不通过/0 仓处理。".to_string() },
|
||||
ManualFunction { name: "deposit_withdraw/finance_repay/management_fee".to_string(), signature: "account.deposit_withdraw(amount, receiving_days=0)".to_string(), detail: "策略账户资金动作。回测中 deposit_withdraw 正数入金、负数出金,receiving_days 大于 0 时按交易日延迟到账,并保持现金流中性净值不把外部资金流当成收益;回测 finance_repay 与 management_fee 按账户合同结算。模拟盘只接受由 runtime 明确返回的即时 deposit_withdraw,并通过幂等现金流账本落库;延迟流、融资/管理费动作必须显式失败。实盘禁止策略侧改变现金,必须以券商资产和已核验资金流水为真相,策略返回上述动作会在下单前 fail-closed。".to_string() },
|
||||
ManualFunction { name: "rolling_mean / sma / ma".to_string(), signature: "rolling_mean(\"field\", lookback) / ma(\"close\", 20)".to_string(), detail: "任意字段滚动均值,支持 close、volume、amount、turnover_ratio、effective_turnover_ratio、signal_open/signal_close、benchmark_open/benchmark_close 和所有数值型 extra_factors。第一个参数必须是字段名或字符串字段名,不能传嵌套表达式或另一个 helper 调用。个股 close 使用当前交易日前已完成收盘序列,volume 使用当前交易日前已完成成交量序列;历史窗口不足时在选股过滤和买入仓位表达式中按不通过/0 仓处理。".to_string() },
|
||||
ManualFunction { name: "vma".to_string(), signature: "vma(60)".to_string(), detail: "rolling_mean(\"volume\", lookback) 的便捷别名,用于任意窗口成交量均线,例如 vma(5) < vma(60)。".to_string() },
|
||||
ManualFunction { name: "rolling_sum / rolling_min / rolling_max".to_string(), signature: "rolling_sum(\"volume\", 20)".to_string(), detail: "任意数值字段滚动求和、最小值、最大值。可用于量能收缩、区间高低点、资金活跃度等过滤或排序。".to_string() },
|
||||
ManualFunction { name: "rolling_stddev / stddev / rolling_zscore / pct_change".to_string(), signature: "stddev(\"close\", 20) / pct_change(\"close\", 10)".to_string(), detail: "滚动标准差、最新值 Z 分数和区间涨跌幅。pct_change(field, n) 会读取 n+1 个窗口点并计算 latest / first - 1。".to_string() },
|
||||
ManualFunction { name: "数据库指标因子".to_string(), signature: "factor_value(\"ths_valid_turnover_stock\", 1)".to_string(), detail: "stock_indicator_factors_v1 中的指标会进入 extra_factors,可用 factor(\"字段\")、factors[\"字段\"]、factor_value(\"字段\", lookback) 或 rolling_mean(\"字段\", n) 读取。市值类指标统一提供亿元口径别名 ths_market_value_stock、ths_market_value_stock_bn、ths_current_mv_stock、ths_current_mv_stock_bn,同时保留 raw 后缀原始值。".to_string() },
|
||||
ManualFunction { name: "rolling_sum / rolling_min / rolling_max".to_string(), signature: "rolling_sum(\"volume\", 20)".to_string(), detail: "任意数值字段滚动求和、最小值、最大值。第一个参数必须是字段名或字符串字段名,不能传嵌套表达式或另一个 helper 调用。可用于量能收缩、区间高低点、资金活跃度等过滤或排序。".to_string() },
|
||||
ManualFunction { name: "rolling_stddev / stddev / rolling_zscore / pct_change".to_string(), signature: "stddev(\"close\", 20) / pct_change(\"close\", 10)".to_string(), detail: "滚动标准差、最新值 Z 分数和区间涨跌幅。第一个参数必须是字段名或字符串字段名,不能传嵌套表达式或另一个 helper 调用;需要收益率波动时先使用已注册收益率字段或发布因子,不要写 rolling_stddev(pct_change(\"close\", 1), 20)。pct_change(field, n) 会读取 n+1 个窗口点并计算 latest / first - 1。".to_string() },
|
||||
ManualFunction { name: "Source Lake 指标因子".to_string(), signature: "factor_value(\"ths_valid_turnover_stock\", 1)".to_string(), detail: "Strategy Factory Source Lake 中已完成 PIT/as-of 审计的 source rows 字段、已发布指标或因子 artifact 会进入 extra_factors,可用 factor(\"字段\")、factors[\"字段\"]、factor_value(\"字段\", lookback) 或 rolling_mean(\"字段\", n) 读取。市值类指标统一提供亿元口径别名 ths_market_value_stock、ths_market_value_stock_bn、ths_current_mv_stock、ths_current_mv_stock_bn,同时保留 raw 后缀原始值。".to_string() },
|
||||
ManualFunction { name: "round/floor/ceil/abs/min/max/clamp".to_string(), signature: "round(x)".to_string(), detail: "常用数值函数。".to_string() },
|
||||
ManualFunction { name: "safe_div".to_string(), signature: "safe_div(lhs, rhs, fallback)".to_string(), detail: "安全除法。".to_string() },
|
||||
ManualFunction { name: "safe_div".to_string(), signature: "safe_div(lhs, rhs) / safe_div(lhs, rhs, fallback)".to_string(), detail: "安全除法,两参数形式默认 fallback=0。".to_string() },
|
||||
ManualFunction { name: "contains/starts_with/ends_with/lower/upper/trim/strlen".to_string(), signature: "starts_with(symbol, \"60\")".to_string(), detail: "字符串辅助函数。".to_string() },
|
||||
],
|
||||
factor_sources: vec![
|
||||
@@ -360,12 +407,12 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
||||
},
|
||||
ManualFactorSource {
|
||||
table: "盘口深度参数".to_string(),
|
||||
detail: "可选字段包括 date、symbol、timestamp、level、bid_price、bid_volume、ask_price、ask_volume。存在盘口深度时,期货 counterparty_offer / next_tick_best_counterparty 可按真实多档盘口逐档扫单;不存在时不会伪造 depth。".to_string(),
|
||||
detail: "可选字段包括 date、symbol、timestamp、level、bid_price、bid_volume、ask_price、ask_volume。存在盘口深度时,期货 minute_best_counterparty 可按真实多档盘口逐档扫单;不存在时不会伪造 depth。".to_string(),
|
||||
fields: vec![],
|
||||
},
|
||||
ManualFactorSource {
|
||||
table: "期货交易参数".to_string(),
|
||||
detail: "字段包括 symbol、effective_date、contract_multiplier、long_margin_rate、short_margin_rate、commission_type、open_commission_ratio、close_commission_ratio、close_today_commission_ratio、price_tick。回测会按交易日自动选择不晚于当前日期的最新参数,用于保证金、手续费和限价 tick 校验。".to_string(),
|
||||
detail: "来自 futures_contract_spec_history 与 futures_cost_margin_history;字段包括 symbol、effective_date、contract_multiplier、long_margin_rate、short_margin_rate、commission_type、open_commission_ratio、close_commission_ratio、close_today_commission_ratio、price_tick。回测按交易日选择不晚于当前日期的最新参数。schema catalog 未同时发布 futures_contract_daily、futures_contract_spec_history、futures_cost_margin_history 时,该能力视为不可用。".to_string(),
|
||||
fields: vec![],
|
||||
},
|
||||
],
|
||||
@@ -383,8 +430,8 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
||||
code: "filter.stock_expr(industry_name(\"citics\", 1) == \"电子\" && factor_text(\"concept\") == \"ai_chip\")".to_string(),
|
||||
},
|
||||
ManualExample {
|
||||
title: "next tick 撮合 + tick 滑点".to_string(),
|
||||
code: "execution.matching_type(\"next_tick_last\")\nexecution.slippage(\"tick_size\", 1)".to_string(),
|
||||
title: "分钟执行价撮合 + 最小价位滑点".to_string(),
|
||||
code: "execution.matching_type(\"minute_last\")\nexecution.slippage(\"tick_size\", 1)".to_string(),
|
||||
},
|
||||
ManualExample {
|
||||
title: "动态 universe 和订阅".to_string(),
|
||||
@@ -432,17 +479,22 @@ pub fn render_manual_markdown(manual: &StrategyAiManual) -> String {
|
||||
out.push_str("## AI 代码生成硬约束\n");
|
||||
out.push_str("- 只输出完整 `engine-script` 代码;第一行必须是 `strategy(\"...\")`、`let`、`fn`、`const` 或 `//`。\n");
|
||||
out.push_str("- 禁止输出 Markdown、解释、推理过程、JSON 包装、手册复述或结果报告。\n");
|
||||
out.push_str("- 只使用支持语句块:`market`、`benchmark`、`signal`、`rebalance.every_days(...).at([...])`、`selection.limit`、`selection.market_cap_band`、`filter.stock_ma`、`filter.stock_expr`、`ordering.rank_by`、`ordering.rank_expr`、`allocation.buy_scale`、`risk.stop_loss`、`risk.take_profit`、`risk.index_exposure`、`execution.matching_type`、`execution.slippage`、`universe.exclude`。\n");
|
||||
out.push_str("- 只使用支持语句块:`market`、`benchmark`、`signal`、`rebalance.every_days(...).at([...])`、`selection.limit`、`selection.market_cap_band`、`filter.stock_ma`、`filter.stock_expr`、`ordering.rank_by`、`ordering.rank_expr`、`allocation.buy_scale`、`risk.stop_loss`、`risk.take_profit`、`risk.index_exposure`、`risk.policy`、`risk.blacklist`、`execution.matching_type`、`execution.rebalance_cash_mode`、`execution.slippage`、`universe.exclude`。\n");
|
||||
out.push_str("- `universe.exclude` 只用于用户明确要求的业务排除项;框架默认的 ST、停牌、退市、新股、科创、一元、涨跌停、同日卖出禁买、成交量、手续费和印花税等基础风控必须写 `risk.policy(...)` 或由运行态 RiskLimits 注入。源策略明确写出的业务选股排除必须保留为策略表达式,不能改写冻结的 selection 风控开关。\n");
|
||||
out.push_str("- 禁止伪 DSL:`filter(...)`、`rank(...)`、`select.top(...)`、`weight.equal(...)`、`sell_rule(...)`、`backtest(...)`、`risk.max_position(...)`。\n");
|
||||
out.push_str("- 市值表达式字段只能用 `market_cap` 或 `free_float_cap`;不要使用数据库原始字段 `float_market_cap`。\n");
|
||||
out.push_str("- 任意窗口价格均线使用 `rolling_mean(\"close\", n)` 或 `ma(\"close\", n)`;任意窗口均量使用 `rolling_mean(\"volume\", n)` 或 `vma(n)`;不要使用未列出的 `ma60`、`stock_ma60`、`signal_ma60` 或 `benchmark_ma60` 变量。\n");
|
||||
out.push_str("- `rolling_mean`、`rolling_sum/min/max/stddev/zscore`、`pct_change`、`factor_value` 等 helper 的第一个参数必须是字段名或字符串字段名;不要输出 `rolling_stddev(pct_change(\"close\", 1), 20)` 这类嵌套表达式。\n");
|
||||
out.push_str("- 自定义 `fn` 必须通过参数传入运行时字段;不要用 `fn score()` 这类零参数函数直接引用 `market_cap`、`close`、`ma5` 等股票字段。\n");
|
||||
out.push_str("- `selection.market_cap_band` 必须写命名参数:`field=\"market_cap\"` 或 `field=\"free_float_cap\"`,并包含 `lower=...` 与 `upper=...`。\n");
|
||||
out.push_str(
|
||||
"- `risk.index_exposure(...)` 只能传一个表达式;不要生成 `risk.exposure(...)`。\n",
|
||||
);
|
||||
out.push_str("- `filter.stock_expr(...)` 只写 alpha 或策略明确声明的业务过滤条件;转换器不得自行把框架默认基础风控注入过滤表达式。源策略明确写出的 `!is_st`、`!is_star_st`、`!is_kcb`、`!is_bjse` 等业务选股排除必须原样保留,同时冻结的 `reject_*_selection` 值不得改变。\n");
|
||||
out.push_str("- 完整三元表达式 `cond ? a : b` 可在表达式参数中使用;若当前运行环境报 `Unknown operator: '?'`,先重编译并重启回测服务,不要改写策略语义掩盖运行时漂移。\n");
|
||||
out.push_str("- `next_bar_open` 的选股、排序和仓位信号来自决策日,订单在下一可交易开盘撮合;不要使用执行日价格作为下单前信号。\n");
|
||||
out.push_str("- `next_bar_open` 必须区分信号日、订单创建日和实际成交日:T 日只生成订单意图,涨跌停、停牌、ST、退市、一元股、黑名单、成交量和盘口流动性等执行约束必须由撮合/风控层按实际成交日判断;涨停买入和跌停卖出必须比较实际 next-open 成交价与涨跌停价,不能用执行日 close/last 或 next-close;禁止用 T 日执行状态拦截 T+1 可交易订单。\n");
|
||||
out.push_str("- 日线目标金额、目标比例和目标权重在 `next_bar_open` 下冻结 T 日收盘目标,T+1 按实际 open、滑点、卖后买延迟滑点、手续费和证券数量步长重算股数;禁止用 T+1 prev_close、T 日估算股数或 T+1 开盘后权益替代。金额预算始终严格,不能生成 `execution.strict_value_budget(false)`。\n");
|
||||
out.push_str("- `execution.matching_type(...)` 和 `execution.slippage(...)` 必须使用手册列出的合法取值。\n\n");
|
||||
out.push_str("## 语句块\n");
|
||||
for item in &manual.statement_blocks {
|
||||
@@ -514,13 +566,19 @@ pub fn build_generation_prompt(
|
||||
prompt.push_str("- 不要输出解释文本。\n");
|
||||
prompt.push_str("- 必须使用 strategy(\"...\") { ... } 语法。\n");
|
||||
prompt.push_str("- 如需自定义参数,使用 let 和 fn。\n");
|
||||
prompt.push_str("- 优先使用数据库已存在字段和 factors[...]。\n\n");
|
||||
prompt.push_str("- 优先使用 Strategy Factory Source Lake 已注册 source rows 字段、已发布指标/因子 artifact 和运行时已存在字段、factors[...];不要回退 ficlaw-data、QuantAPI、旧数据中心 HTTP、ClickHouse 或临时文件。\n\n");
|
||||
prompt.push_str("- 生成的代码必须能转换为 strategy_spec 并提交 POST /v1/backtests。\n");
|
||||
prompt.push_str("- 用户指定“持仓N只、目标持仓N、stocknum=N、selection.limit(N)”时,必须把最终持仓槽位写成 N;用户指定“至少/不少于N只”时,最终持仓槽位必须 >= N。\n");
|
||||
prompt.push_str("- ");
|
||||
prompt.push_str(PERFORMANCE_ACCEPTANCE_CONTRACT_PROMPT);
|
||||
prompt.push('\n');
|
||||
prompt.push_str("- 不要使用手册未列出的字段、函数或外部平台 API 名称。\n\n");
|
||||
prompt.push_str("只允许使用这些可编译语句:market、benchmark、signal、rebalance.every_days(...).at([...])、selection.limit、selection.market_cap_band、filter.stock_ma、filter.stock_expr、ordering.rank_by、ordering.rank_expr、allocation.buy_scale、risk.stop_loss、risk.take_profit、risk.index_exposure、execution.matching_type、execution.slippage、universe.exclude。禁止输出 filter(...)、rank(...)、select.top(...)、weight.equal()、sell_rule(...)、backtest(...)、risk.max_position(...) 这类未支持伪语法。\n");
|
||||
prompt.push_str("参数形态必须严格:selection.market_cap_band 必须写 field=\"market_cap\" 或 field=\"free_float_cap\", lower=..., upper=...;禁止使用 float_market_cap;禁止使用 ma60、stock_ma60、signal_ma60、benchmark_ma60,60日价格均线写 rolling_mean(\"close\", 60) 或 ma(\"close\", 60),任意窗口均量写 rolling_mean(\"volume\", n) 或 vma(n);不要生成 fn score() 这类零参数函数,股票字段排序直接写在 ordering.rank_expr 内或用带参数函数;布尔字段按布尔使用,写 !is_st、!paused、!at_upper_limit、!at_lower_limit,不要写 is_st == 0;risk.index_exposure 只能传一个数值表达式,不要使用 risk.exposure;完整三元表达式 cond ? a : b 可以使用,但不得输出残缺问号/冒号片段;execution.matching_type 只能取 next_tick_last、next_tick_best_own、next_tick_best_counterparty、counterparty_offer、vwap、current_bar_close、next_bar_open、open_auction;next_bar_open 只能使用决策日信号,不能把执行日价格当作下单前信息;execution.slippage 必须写 execution.slippage(\"none\") 或 execution.slippage(\"price_ratio\", 0.001)。\n");
|
||||
prompt.push_str("只允许使用这些可编译语句:market、benchmark、signal、rebalance.every_days(...).at([...])、selection.limit、selection.market_cap_band、filter.stock_ma、filter.stock_expr、ordering.rank_by、ordering.rank_expr、allocation.buy_scale、risk.stop_loss、risk.take_profit、risk.index_exposure、risk.policy、risk.blacklist、execution.matching_type、execution.rebalance_cash_mode、execution.slippage、universe.exclude。universe.exclude 只用于用户明确要求的业务排除项,不能表达 FIDC 基础风控。禁止输出 filter(...)、rank(...)、select.top(...)、weight.equal()、sell_rule(...)、backtest(...)、risk.max_position(...) 这类未支持伪语法。\n");
|
||||
prompt.push_str(&format!("参数形态必须严格:selection.market_cap_band 必须写 field=\"market_cap\" 或 field=\"free_float_cap\", lower=..., upper=...;禁止使用 float_market_cap;禁止使用 ma60、stock_ma60、signal_ma60、benchmark_ma60,60日价格均线写 rolling_mean(\"close\", 60) 或 ma(\"close\", 60),任意窗口均量写 rolling_mean(\"volume\", n) 或 vma(n);rolling_mean、rolling_sum/min/max/stddev/zscore、pct_change、factor_value 等 helper 的第一个参数必须是字段名或字符串字段名,不能传嵌套表达式或另一个 helper 调用;不要生成 fn score() 这类零参数函数,股票字段排序直接写在 ordering.rank_expr 内或用带参数函数;布尔字段按布尔使用,不要写 is_st == 0;filter.stock_expr 只写 alpha 或业务过滤条件,不要把 !is_st、!paused、!at_upper_limit、!at_lower_limit 这类基础风控散落在表达式里;risk.index_exposure 只能传一个数值表达式,不要使用 risk.exposure;risk.policy 只写 FIDC 基础风控、成交量和交易成本命名参数,必须覆盖完整默认配置面,例如 {DEFAULT_RISK_POLICY_DSL_PROMPT},不要用它表达策略择时或收益规则;完整三元表达式 cond ? a : b 可以使用,但不得输出残缺问号/冒号片段;日线回测 execution.matching_type 只能取 current_bar_close 或 next_bar_open,分钟线回测只能取 minute_last;日线 execution.rebalance_cash_mode 只允许 same_point_net、sell_then_buy、pre_open_cash,分钟线固定使用 sell_then_buy;不要把 vwap、twap、open_auction、minute_best_own、minute_best_counterparty 写成全局 matching_type;next_bar_open 只能使用决策日信号,不能把执行日价格当作下单前信息;next_bar_open 下 T 日只生成订单意图并在收盘冻结目标金额或目标权益,T+1 按实际 open、滑点、手续费和证券数量步长重算股数,不能用 T+1 prev_close 或 T 日估算股数直接成交;涨跌停、停牌、ST、退市、一元股、黑名单、成交量和盘口流动性等执行约束必须由撮合/风控层按实际成交日判断;涨停买入和跌停卖出必须用实际 next-open 成交价比较,不能用执行日 close/last 或 next-close;禁止用 T 日执行状态拦截 T+1 可交易订单;金额预算始终严格,禁止 execution.strict_value_budget(false);execution.slippage 必须写 execution.slippage(\"none\") 或 execution.slippage(\"price_ratio\", 请求滑点率,例如 0.002),并且请求里指定固定滑点时必须使用请求值。\n"));
|
||||
prompt.push_str("回测成功但 tradeCount=0 或 holdingCount=0 是无效策略;第一版必须保持稳定买入覆盖率,复杂因子只能在后续优化中逐步加严。\n");
|
||||
prompt.push_str("可参考但不要照抄的最小模板,回复时不要包含 ``` 代码围栏:\nstrategy(\"cn_a_smallcap_factor_rotation\") {\nmarket(\"CN_A\")\nbenchmark(\"000852.SH\")\nsignal(\"000001.SH\")\nrebalance.every_days(5).at([\"10:18\"])\nselection.limit(40)\nselection.market_cap_band(field=\"market_cap\", lower=0, upper=1000)\nfilter.stock_expr(listed_days >= 60 && !is_st && !paused && close > 2 && !at_upper_limit && !at_lower_limit)\nordering.rank_by(\"market_cap\", \"asc\")\nallocation.buy_scale(1.0)\nrisk.index_exposure(1.0)\nrisk.stop_loss(holding_return < -0.08)\nexecution.slippage(\"price_ratio\", 0.001)\n}\n\n");
|
||||
prompt.push_str("可参考但不要照抄的最小模板,回复时不要包含 ``` 代码围栏:\nstrategy(\"cn_a_smallcap_factor_rotation\") {\nmarket(\"CN_A\")\nbenchmark(\"000852.SH\")\nsignal(\"000001.SH\")\nrebalance.every_days(5).at([\"10:18\"])\nselection.limit(40)\nselection.market_cap_band(field=\"market_cap\", lower=0, upper=1000)\nfilter.stock_expr(listed_days >= 60 && close > 2)\nordering.rank_by(\"market_cap\", \"asc\")\nallocation.buy_scale(1.0)\nrisk.policy(");
|
||||
prompt.push_str(DEFAULT_RISK_POLICY_DSL_CODE);
|
||||
prompt.push_str(")\nrisk.index_exposure(1.0)\nrisk.stop_loss(holding_return < -0.08)\nexecution.slippage(\"price_ratio\", 0.002)\n}\n\n");
|
||||
prompt.push_str("用户目标:\n");
|
||||
prompt.push_str(&format!("- {}\n", request.user_goal));
|
||||
if !request.constraints.is_empty() {
|
||||
@@ -546,8 +604,11 @@ pub fn build_optimization_prompt(
|
||||
prompt.push_str("你是 OmniQuant 平台策略脚本优化器。必须输出完整、可运行的平台策略脚本,不要输出解释文本。\n");
|
||||
prompt.push_str("输出格式硬约束:回复第一行必须是 strategy(\"...\")、let、fn、const 或 //;回复中不得包含 Markdown、解释、思考过程、手册复述、JSON 包装或自然语言总结。\n");
|
||||
prompt.push_str("长度硬约束:策略代码目标 80 行以内,只保留必要 let/fn/strategy 块;不要复制下面的手册片段、历史策略全文或字段清单。\n");
|
||||
prompt.push_str("只修改与优化目标相关的少量参数或过滤条件,保留原策略的市场、基准、信号指数和核心风控;不要引入手册未列出的字段或外部平台 API 名称。\n");
|
||||
prompt.push_str("优化可以调整调仓周期、持仓数、市值带、filter.stock_expr、ordering.rank_expr、allocation.buy_scale、止盈止损;如上一轮无交易或质量分过低,必须先放宽过滤条件并优先使用已入库指标因子、rolling_mean/ma/vma/rolling_stddev/pct_change 等支持函数。\n");
|
||||
prompt.push_str("优化不限制在原策略已有参数或少量扰动。只要 OmniQuant/FIDC 已支持,可以自由增加、修改、删除策略代码、参数、候选池、过滤函数、排序、仓位、止盈止损、调仓周期、指标因子和辅助函数;不得引入手册未列出的字段或外部平台 API 名称。\n");
|
||||
prompt.push_str("持仓数量属于策略合同,不是优化自由参数。原策略或用户目标明确 stocknum、selection.limit、目标持仓N只或不少于N只时,优化后必须保留该目标槽位或满足最低槽位,不能为了收益或交易次数擅自改小。\n");
|
||||
prompt.push_str(PERFORMANCE_ACCEPTANCE_CONTRACT_PROMPT);
|
||||
prompt.push('\n');
|
||||
prompt.push_str("可以使用 Strategy Factory Source Lake 已注册并完成 PIT/as-of 审计的日频 source rows 字段、已发布指标/因子 artifact 和表达式函数,例如 rolling_mean/ma/vma/rolling_sum/rolling_stddev/pct_change/factor/factor_value/factors;这些滚动/因子 helper 的字段参数只能是字段名或字符串字段名,不要嵌套表达式;不要回退 ficlaw-data、QuantAPI、旧数据中心 HTTP、ClickHouse 或临时文件。如上一轮无交易或质量分过低,必须先扩大候选覆盖并修正不可交易过滤,再优化收益。\n");
|
||||
prompt.push_str("优化目标:\n");
|
||||
prompt.push_str(&format!("- {}\n\n", request.objective));
|
||||
prompt.push_str("当前策略代码如下,仅作为输入参考;回复时不要包含 Markdown 代码围栏:\n");
|
||||
@@ -569,3 +630,81 @@ pub fn build_optimization_prompt(
|
||||
prompt.push_str(manual_markdown);
|
||||
prompt
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn generation_prompt_uses_explicit_performance_acceptance_contract() {
|
||||
let prompt = build_generation_prompt(
|
||||
"manual",
|
||||
&StrategyAiGenerateRequest {
|
||||
user_goal: "生成策略".to_string(),
|
||||
constraints: Vec::new(),
|
||||
market: "CN_A".to_string(),
|
||||
benchmark_symbol: "000852.SH".to_string(),
|
||||
signal_symbol: "000001.SH".to_string(),
|
||||
holding_count_contract: None,
|
||||
},
|
||||
);
|
||||
|
||||
assert!(prompt.contains("只能来自用户目标、请求约束或不可变 candidate/promotion contract"));
|
||||
assert!(prompt.contains("不得注入 120% 或其他默认数值"));
|
||||
assert!(!prompt.contains("总收益严格 > 120%"));
|
||||
assert!(prompt.contains("Strategy Factory Source Lake 已注册 source rows 字段"));
|
||||
assert!(prompt.contains("不要回退 ficlaw-data"));
|
||||
assert!(prompt.contains("ClickHouse"));
|
||||
assert!(prompt.contains("T 日只生成订单意图"));
|
||||
assert!(prompt.contains("按实际成交日判断"));
|
||||
assert!(prompt.contains("禁止用 T 日执行状态拦截 T+1 可交易订单"));
|
||||
assert!(prompt.contains("execution.rebalance_cash_mode"));
|
||||
assert!(prompt.contains("same_point_net、sell_then_buy、pre_open_cash"));
|
||||
assert!(prompt.contains("分钟线固定使用 sell_then_buy"));
|
||||
assert!(prompt.contains("必须覆盖完整默认配置面"));
|
||||
assert!(prompt.contains("reject_inactive_buy=true"));
|
||||
assert!(prompt.contains("reject_inactive_sell=true"));
|
||||
assert!(prompt.contains("reject_new_listing_buy=true"));
|
||||
assert!(prompt.contains("reject_kcb_buy=true"));
|
||||
assert!(prompt.contains("reject_bjse_buy=true"));
|
||||
assert!(prompt.contains("reject_one_yuan_buy=true"));
|
||||
assert!(prompt.contains("respect_allow_buy_sell=true"));
|
||||
assert!(prompt.contains("stamp_tax_rate_before_change=0.001"));
|
||||
assert!(prompt.contains("stamp_tax_change_date=\"2023-08-28\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manual_separates_explicit_business_selection_from_framework_risk_policy() {
|
||||
let markdown = render_manual_markdown(&built_in_strategy_manual());
|
||||
assert!(markdown.contains("源策略明确写出的业务选股排除属于策略本身"));
|
||||
assert!(markdown.contains("不能反向修改冻结的 reject_*_selection 开关"));
|
||||
assert!(markdown.contains("冻结的 `reject_*_selection` 值不得改变"));
|
||||
assert!(markdown.contains("time_in_force=\"day|ioc|fok|gtc\""));
|
||||
assert!(markdown.contains("FOK 必须全量可成交否则零成交"));
|
||||
assert!(markdown.contains("GTC 仅支持限价单并跨交易日保留"));
|
||||
assert!(markdown.contains("paper/live 当前只支持 DAY/IOC/FOK"));
|
||||
assert!(markdown.contains("paper/live 必须明确拒绝并禁止降级为 DAY"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optimization_prompt_uses_explicit_performance_acceptance_contract() {
|
||||
let prompt = build_optimization_prompt(
|
||||
"manual",
|
||||
&StrategyAiOptimizeRequest {
|
||||
current_code: "strategy(\"demo\") {}".to_string(),
|
||||
objective: "优化收益".to_string(),
|
||||
result_summary: json!({ "total_return": 1.49 }),
|
||||
diagnostics: Vec::new(),
|
||||
holding_count_contract: None,
|
||||
},
|
||||
);
|
||||
|
||||
assert!(prompt.contains("只能来自用户目标、请求约束或不可变 candidate/promotion contract"));
|
||||
assert!(prompt.contains("不得注入 120% 或其他默认数值"));
|
||||
assert!(!prompt.contains("总收益严格 > 120%"));
|
||||
assert!(prompt.contains("Strategy Factory Source Lake 已注册并完成 PIT/as-of 审计"));
|
||||
assert!(prompt.contains("不要回退 ficlaw-data"));
|
||||
assert!(prompt.contains("ClickHouse"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use chrono::NaiveDate;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::data::{BenchmarkSnapshot, DataSet, EligibleUniverseSnapshot};
|
||||
use crate::risk_control::{ChinaAShareRiskControl, FidcRiskControlConfig, FidcRiskDecisionAudit};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BandRegime {
|
||||
@@ -39,6 +40,7 @@ pub struct SelectionDiagnostics {
|
||||
pub missing_market_cap_symbols: Vec<String>,
|
||||
pub selected_symbols: Vec<String>,
|
||||
pub rejection_examples: Vec<String>,
|
||||
pub risk_decisions: Vec<FidcRiskDecisionAudit>,
|
||||
}
|
||||
|
||||
pub struct SelectionContext<'a> {
|
||||
@@ -47,20 +49,62 @@ pub struct SelectionContext<'a> {
|
||||
pub reference_level: f64,
|
||||
pub data: &'a DataSet,
|
||||
pub dynamic_universe: Option<&'a BTreeSet<String>>,
|
||||
pub risk_config: Option<&'a FidcRiskControlConfig>,
|
||||
pub defer_selection_risk: bool,
|
||||
}
|
||||
|
||||
impl SelectionContext<'_> {
|
||||
fn eligible_universe(&self) -> Vec<EligibleUniverseSnapshot> {
|
||||
let eligible = self.data.eligible_universe_on(self.decision_date);
|
||||
let eligible = match (self.risk_config, self.defer_selection_risk) {
|
||||
(Some(risk_config), false) => self
|
||||
.data
|
||||
.eligible_universe_on_with_risk_config(self.decision_date, risk_config),
|
||||
_ => self.data.eligible_universe_on(self.decision_date).to_vec(),
|
||||
};
|
||||
match self.dynamic_universe {
|
||||
Some(symbols) if !symbols.is_empty() => eligible
|
||||
.iter()
|
||||
.into_iter()
|
||||
.filter(|row| symbols.contains(&row.symbol))
|
||||
.cloned()
|
||||
.collect(),
|
||||
_ => eligible.to_vec(),
|
||||
_ => eligible,
|
||||
}
|
||||
}
|
||||
|
||||
fn selection_risk_decisions(&self) -> Vec<FidcRiskDecisionAudit> {
|
||||
let default_risk_config;
|
||||
let risk_config = match self.risk_config {
|
||||
Some(value) => value,
|
||||
None => {
|
||||
default_risk_config = FidcRiskControlConfig::default();
|
||||
&default_risk_config
|
||||
}
|
||||
};
|
||||
let mut decisions = Vec::new();
|
||||
for factor in self.data.factor_snapshot_rows_on(self.decision_date) {
|
||||
if self
|
||||
.dynamic_universe
|
||||
.is_some_and(|symbols| !symbols.is_empty() && !symbols.contains(&factor.symbol))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let Some(candidate) = self.data.candidate(self.decision_date, &factor.symbol) else {
|
||||
continue;
|
||||
};
|
||||
let Some(market) = self.data.market(self.decision_date, &factor.symbol) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(decision) = ChinaAShareRiskControl::selection_rejection_decision_with_config(
|
||||
self.decision_date,
|
||||
candidate,
|
||||
market,
|
||||
self.data.instrument(&factor.symbol),
|
||||
risk_config,
|
||||
) {
|
||||
decisions.push(decision);
|
||||
}
|
||||
}
|
||||
decisions
|
||||
}
|
||||
}
|
||||
|
||||
pub trait UniverseSelector {
|
||||
@@ -166,9 +210,23 @@ impl UniverseSelector for DynamicMarketCapBandSelector {
|
||||
missing_market_cap_symbols: Vec::new(),
|
||||
selected_symbols: Vec::new(),
|
||||
rejection_examples: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
};
|
||||
|
||||
diagnostics.factor_total = ctx.data.factor_snapshots_on(ctx.decision_date).len();
|
||||
diagnostics.factor_total = ctx.data.factor_snapshot_rows_on(ctx.decision_date).len();
|
||||
diagnostics.risk_decisions = ctx.selection_risk_decisions();
|
||||
diagnostics.not_eligible_count = diagnostics.risk_decisions.len();
|
||||
diagnostics.paused_count = diagnostics
|
||||
.risk_decisions
|
||||
.iter()
|
||||
.filter(|decision| decision.rule_code == "paused")
|
||||
.count();
|
||||
diagnostics.rejection_examples = diagnostics
|
||||
.risk_decisions
|
||||
.iter()
|
||||
.take(8)
|
||||
.map(|decision| format!("{} rejected by {}", decision.symbol, decision.rule_code))
|
||||
.collect();
|
||||
let eligible = ctx.eligible_universe();
|
||||
diagnostics.market_cap_missing_count =
|
||||
diagnostics.factor_total.saturating_sub(eligible.len());
|
||||
@@ -221,3 +279,267 @@ fn to_universe_candidate(
|
||||
band_high,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::data::{
|
||||
BenchmarkSnapshot, CandidateEligibility, DailyFactorSnapshot, DailyMarketSnapshot, DataSet,
|
||||
};
|
||||
use crate::instrument::Instrument;
|
||||
|
||||
fn d() -> NaiveDate {
|
||||
NaiveDate::from_ymd_opt(2025, 1, 2).unwrap()
|
||||
}
|
||||
|
||||
fn instrument(symbol: &str) -> Instrument {
|
||||
Instrument {
|
||||
symbol: symbol.to_string(),
|
||||
name: symbol.to_string(),
|
||||
board: symbol.rsplit('.').next().unwrap_or("").to_string(),
|
||||
round_lot: 100,
|
||||
listed_at: Some(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()),
|
||||
delisted_at: None,
|
||||
status: "active".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn market(symbol: &str, price: f64) -> DailyMarketSnapshot {
|
||||
DailyMarketSnapshot {
|
||||
date: d(),
|
||||
symbol: symbol.to_string(),
|
||||
timestamp: Some("2025-01-02 10:00:00".to_string()),
|
||||
day_open: price,
|
||||
open: price,
|
||||
high: price,
|
||||
low: price,
|
||||
close: price,
|
||||
last_price: price,
|
||||
bid1: price,
|
||||
ask1: price,
|
||||
prev_close: price,
|
||||
volume: 1_000_000,
|
||||
minute_volume: 10_000,
|
||||
bid1_volume: 10_000,
|
||||
ask1_volume: 10_000,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
paused: false,
|
||||
upper_limit: price * 1.1,
|
||||
lower_limit: price * 0.9,
|
||||
price_tick: 0.01,
|
||||
}
|
||||
}
|
||||
|
||||
fn factor(symbol: &str, market_cap_bn: f64) -> DailyFactorSnapshot {
|
||||
DailyFactorSnapshot {
|
||||
date: d(),
|
||||
symbol: symbol.to_string(),
|
||||
market_cap_bn,
|
||||
free_float_cap_bn: market_cap_bn,
|
||||
pe_ttm: 10.0,
|
||||
turnover_ratio: Some(0.01),
|
||||
effective_turnover_ratio: Some(0.01),
|
||||
adjustment_factor_backward1: None,
|
||||
extra_factors: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn candidate(symbol: &str, is_st: bool, is_kcb: bool) -> CandidateEligibility {
|
||||
CandidateEligibility {
|
||||
date: d(),
|
||||
symbol: symbol.to_string(),
|
||||
is_st,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
is_paused: false,
|
||||
allow_buy: true,
|
||||
allow_sell: true,
|
||||
is_kcb,
|
||||
is_one_yuan: false,
|
||||
risk_level_code: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn benchmark() -> BenchmarkSnapshot {
|
||||
BenchmarkSnapshot {
|
||||
date: d(),
|
||||
benchmark: "000852.SH".to_string(),
|
||||
open: 2000.0,
|
||||
close: 2000.0,
|
||||
prev_close: 1990.0,
|
||||
volume: 1_000_000,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selector_records_structured_selection_risk_decisions() {
|
||||
let data = DataSet::from_components(
|
||||
vec![
|
||||
instrument("000001.SZ"),
|
||||
instrument("688001.SH"),
|
||||
instrument("000002.SZ"),
|
||||
],
|
||||
vec![
|
||||
market("000001.SZ", 10.0),
|
||||
market("688001.SH", 10.0),
|
||||
market("000002.SZ", 10.0),
|
||||
],
|
||||
vec![
|
||||
factor("000001.SZ", 8.0),
|
||||
factor("688001.SH", 9.0),
|
||||
factor("000002.SZ", 10.0),
|
||||
],
|
||||
vec![
|
||||
candidate("000001.SZ", true, false),
|
||||
candidate("688001.SH", false, true),
|
||||
candidate("000002.SZ", false, false),
|
||||
],
|
||||
vec![benchmark()],
|
||||
)
|
||||
.unwrap();
|
||||
let selector = DynamicMarketCapBandSelector::new(2000.0, 7.0, 10.0, 0.0, 10, 0.0, 0.0, 0.0);
|
||||
let mut risk_config = FidcRiskControlConfig::default();
|
||||
risk_config.static_rules.reject_st_selection = true;
|
||||
risk_config.static_rules.reject_kcb_selection = true;
|
||||
let (_selected, diagnostics) = selector.select_with_diagnostics(&SelectionContext {
|
||||
decision_date: d(),
|
||||
benchmark: &benchmark(),
|
||||
reference_level: 2000.0,
|
||||
data: &data,
|
||||
dynamic_universe: None,
|
||||
risk_config: Some(&risk_config),
|
||||
defer_selection_risk: false,
|
||||
});
|
||||
|
||||
let rules = diagnostics
|
||||
.risk_decisions
|
||||
.iter()
|
||||
.map(|decision| decision.rule_code.as_str())
|
||||
.collect::<BTreeSet<_>>();
|
||||
assert!(rules.contains("st"), "{:?}", diagnostics.risk_decisions);
|
||||
assert!(rules.contains("kcb"), "{:?}", diagnostics.risk_decisions);
|
||||
assert_eq!(
|
||||
diagnostics.not_eligible_count,
|
||||
diagnostics.risk_decisions.len()
|
||||
);
|
||||
assert!(
|
||||
diagnostics.risk_decisions[0]
|
||||
.diagnostic_line()
|
||||
.starts_with("risk_decision=")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selector_applies_configured_selection_risk_on_decision_date() {
|
||||
let data = DataSet::from_components(
|
||||
vec![
|
||||
instrument("000001.SZ"),
|
||||
instrument("688001.SH"),
|
||||
instrument("000002.SZ"),
|
||||
],
|
||||
vec![
|
||||
market("000001.SZ", 10.0),
|
||||
market("688001.SH", 10.0),
|
||||
market("000002.SZ", 10.0),
|
||||
],
|
||||
vec![
|
||||
factor("000001.SZ", 8.0),
|
||||
factor("688001.SH", 9.0),
|
||||
factor("000002.SZ", 10.0),
|
||||
],
|
||||
vec![
|
||||
candidate("000001.SZ", true, false),
|
||||
candidate("688001.SH", false, true),
|
||||
candidate("000002.SZ", false, false),
|
||||
],
|
||||
vec![benchmark()],
|
||||
)
|
||||
.unwrap();
|
||||
let selector = DynamicMarketCapBandSelector::new(2000.0, 7.0, 10.0, 0.0, 10, 0.0, 0.0, 0.0);
|
||||
let mut risk_config = FidcRiskControlConfig::default();
|
||||
risk_config.static_rules.reject_st_selection = true;
|
||||
risk_config.static_rules.reject_kcb_selection = true;
|
||||
let (selected, diagnostics) = selector.select_with_diagnostics(&SelectionContext {
|
||||
decision_date: d(),
|
||||
benchmark: &benchmark(),
|
||||
reference_level: 2000.0,
|
||||
data: &data,
|
||||
dynamic_universe: None,
|
||||
risk_config: Some(&risk_config),
|
||||
defer_selection_risk: false,
|
||||
});
|
||||
|
||||
let selected_symbols = selected
|
||||
.iter()
|
||||
.map(|candidate| candidate.symbol.as_str())
|
||||
.collect::<BTreeSet<_>>();
|
||||
assert!(!selected_symbols.contains("000001.SZ"));
|
||||
assert!(!selected_symbols.contains("688001.SH"));
|
||||
assert!(selected_symbols.contains("000002.SZ"));
|
||||
assert_eq!(diagnostics.not_eligible_count, 2);
|
||||
let rules = diagnostics
|
||||
.risk_decisions
|
||||
.iter()
|
||||
.map(|decision| decision.rule_code.as_str())
|
||||
.collect::<BTreeSet<_>>();
|
||||
assert!(rules.contains("st"), "{:?}", diagnostics.risk_decisions);
|
||||
assert!(rules.contains("kcb"), "{:?}", diagnostics.risk_decisions);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selector_can_defer_configured_selection_risk_without_losing_diagnostics() {
|
||||
let data = DataSet::from_components(
|
||||
vec![
|
||||
instrument("000001.SZ"),
|
||||
instrument("688001.SH"),
|
||||
instrument("000002.SZ"),
|
||||
],
|
||||
vec![
|
||||
market("000001.SZ", 10.0),
|
||||
market("688001.SH", 10.0),
|
||||
market("000002.SZ", 10.0),
|
||||
],
|
||||
vec![
|
||||
factor("000001.SZ", 8.0),
|
||||
factor("688001.SH", 9.0),
|
||||
factor("000002.SZ", 10.0),
|
||||
],
|
||||
vec![
|
||||
candidate("000001.SZ", true, false),
|
||||
candidate("688001.SH", false, true),
|
||||
candidate("000002.SZ", false, false),
|
||||
],
|
||||
vec![benchmark()],
|
||||
)
|
||||
.unwrap();
|
||||
let selector = DynamicMarketCapBandSelector::new(2000.0, 7.0, 10.0, 0.0, 10, 0.0, 0.0, 0.0);
|
||||
let mut risk_config = FidcRiskControlConfig::default();
|
||||
risk_config.static_rules.reject_st_selection = true;
|
||||
risk_config.static_rules.reject_kcb_selection = true;
|
||||
|
||||
let (selected, diagnostics) = selector.select_with_diagnostics(&SelectionContext {
|
||||
decision_date: d(),
|
||||
benchmark: &benchmark(),
|
||||
reference_level: 2000.0,
|
||||
data: &data,
|
||||
dynamic_universe: None,
|
||||
risk_config: Some(&risk_config),
|
||||
defer_selection_risk: true,
|
||||
});
|
||||
|
||||
let selected_symbols = selected
|
||||
.iter()
|
||||
.map(|candidate| candidate.symbol.as_str())
|
||||
.collect::<BTreeSet<_>>();
|
||||
assert!(selected_symbols.contains("000001.SZ"));
|
||||
assert!(selected_symbols.contains("688001.SH"));
|
||||
assert!(selected_symbols.contains("000002.SZ"));
|
||||
let rules = diagnostics
|
||||
.risk_decisions
|
||||
.iter()
|
||||
.map(|decision| decision.rule_code.as_str())
|
||||
.collect::<BTreeSet<_>>();
|
||||
assert!(rules.contains("st"), "{:?}", diagnostics.risk_decisions);
|
||||
assert!(rules.contains("kcb"), "{:?}", diagnostics.risk_decisions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,12 +17,14 @@ fn candidate() -> CandidateEligibility {
|
||||
date: d(2024, 1, 3),
|
||||
symbol: "000001.SZ".to_string(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
is_paused: false,
|
||||
allow_buy: true,
|
||||
allow_sell: true,
|
||||
is_kcb: false,
|
||||
is_one_yuan: false,
|
||||
risk_level_code: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +43,7 @@ fn snapshot(open: f64, upper_limit: f64, lower_limit: f64) -> DailyMarketSnapsho
|
||||
ask1: open,
|
||||
prev_close: 10.0,
|
||||
volume: 1_000_000,
|
||||
tick_volume: 100_000,
|
||||
minute_volume: 100_000,
|
||||
bid1_volume: 50_000,
|
||||
ask1_volume: 50_000,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
@@ -65,6 +67,29 @@ fn china_cost_model_applies_minimum_commission_and_stamp_tax() {
|
||||
assert!((sell.stamp_tax - 100.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_cost_model_matches_declared_run_options() {
|
||||
let model =
|
||||
ChinaAShareCostModel::from_trading_constraints(fidc_core::TradingConstraintConfig {
|
||||
commission_rate: 0.0003,
|
||||
minimum_commission: 5.0,
|
||||
stamp_tax_rate_before_change: 0.0005,
|
||||
stamp_tax_rate_after_change: 0.0005,
|
||||
..fidc_core::TradingConstraintConfig::default()
|
||||
});
|
||||
|
||||
let buy = model.calculate(d(2026, 5, 19), OrderSide::Buy, 49_978.84);
|
||||
assert!((buy.commission - 14.993652).abs() < 1e-9);
|
||||
assert_eq!(buy.stamp_tax, 0.0);
|
||||
|
||||
let sell = model.calculate(d(2026, 5, 19), OrderSide::Sell, 100_724.72);
|
||||
assert!((sell.commission - 30.217416).abs() < 1e-9);
|
||||
assert!((sell.stamp_tax - 50.36236).abs() < 1e-9);
|
||||
|
||||
let small_buy = model.calculate(d(2026, 5, 19), OrderSide::Buy, 1_000.0);
|
||||
assert!((small_buy.commission - 5.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn china_cost_model_switches_stamp_tax_rate_after_2023_08_28() {
|
||||
let model = ChinaAShareCostModel::default();
|
||||
@@ -175,7 +200,7 @@ fn china_rule_hooks_block_buy_at_limit_up_and_sell_at_limit_down() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn china_rule_hooks_use_tick_size_tolerance_for_price_limits() {
|
||||
fn china_rule_hooks_use_strict_price_limits() {
|
||||
let hooks = ChinaEquityRuleHooks;
|
||||
let candidate = candidate();
|
||||
|
||||
@@ -184,6 +209,13 @@ fn china_rule_hooks_use_tick_size_tolerance_for_price_limits() {
|
||||
..snapshot(10.9995, 11.0, 9.0)
|
||||
};
|
||||
let buy_check = hooks.can_buy(d(2024, 1, 3), &near_upper, &candidate, PriceField::Open);
|
||||
assert!(buy_check.allowed);
|
||||
|
||||
let exact_upper = DailyMarketSnapshot {
|
||||
price_tick: 0.001,
|
||||
..snapshot(11.0, 11.0, 9.0)
|
||||
};
|
||||
let buy_check = hooks.can_buy(d(2024, 1, 3), &exact_upper, &candidate, PriceField::Open);
|
||||
assert!(!buy_check.allowed);
|
||||
|
||||
let near_lower = DailyMarketSnapshot {
|
||||
@@ -199,6 +231,19 @@ fn china_rule_hooks_use_tick_size_tolerance_for_price_limits() {
|
||||
&position,
|
||||
PriceField::Open,
|
||||
);
|
||||
assert!(sell_check.allowed);
|
||||
|
||||
let exact_lower = DailyMarketSnapshot {
|
||||
price_tick: 0.001,
|
||||
..snapshot(9.0, 11.0, 9.0)
|
||||
};
|
||||
let sell_check = hooks.can_sell(
|
||||
d(2024, 1, 3),
|
||||
&exact_lower,
|
||||
&candidate,
|
||||
&position,
|
||||
PriceField::Open,
|
||||
);
|
||||
assert!(!sell_check.allowed);
|
||||
}
|
||||
|
||||
@@ -223,7 +268,7 @@ fn china_rule_hooks_allow_sell_when_last_price_is_above_lower_limit() {
|
||||
ask1: 2.53,
|
||||
prev_close: 2.80,
|
||||
volume: 1_000_000,
|
||||
tick_volume: 100_000,
|
||||
minute_volume: 100_000,
|
||||
bid1_volume: 50_000,
|
||||
ask1_volume: 50_000,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
|
||||
@@ -49,14 +49,30 @@ fn portfolio_settles_cash_receivable_on_payable_date() {
|
||||
amount: 500.0,
|
||||
reason: "cash_dividend 0.5".to_string(),
|
||||
});
|
||||
portfolio.add_cash_receivable(CashReceivable {
|
||||
symbol: "000002.SZ".to_string(),
|
||||
ex_date: d(2025, 1, 2),
|
||||
payable_date: d(2025, 1, 5),
|
||||
amount: 250.0,
|
||||
reason: "cash_dividend 0.25".to_string(),
|
||||
});
|
||||
|
||||
let settled_early = portfolio.settle_cash_receivables(d(2025, 1, 4));
|
||||
assert!(settled_early.is_empty());
|
||||
let due_early = portfolio.take_due_cash_receivables(d(2025, 1, 4));
|
||||
assert!(due_early.is_empty());
|
||||
assert!((portfolio.cash() - 1_000_000.0).abs() < 1e-9);
|
||||
|
||||
let settled = portfolio.settle_cash_receivables(d(2025, 1, 5));
|
||||
assert_eq!(settled.len(), 1);
|
||||
assert!((portfolio.cash() - 1_000_500.0).abs() < 1e-9);
|
||||
let due = portfolio.take_due_cash_receivables(d(2025, 1, 5));
|
||||
assert_eq!(due.len(), 2);
|
||||
let mut cash_chain = Vec::new();
|
||||
for receivable in &due {
|
||||
let cash_before = portfolio.cash();
|
||||
portfolio.settle_cash_receivable(receivable).unwrap();
|
||||
cash_chain.push((cash_before, portfolio.cash()));
|
||||
}
|
||||
assert_eq!(
|
||||
cash_chain,
|
||||
vec![(1_000_000.0, 1_000_500.0), (1_000_500.0, 1_000_750.0)]
|
||||
);
|
||||
assert!(portfolio.cash_receivables().is_empty());
|
||||
}
|
||||
|
||||
@@ -74,6 +90,7 @@ impl Strategy for BuyAndHoldStrategy {
|
||||
ctx: &StrategyContext<'_>,
|
||||
) -> Result<StrategyDecision, fidc_core::BacktestError> {
|
||||
Ok(StrategyDecision {
|
||||
buy_denials: Default::default(),
|
||||
rebalance: false,
|
||||
target_weights: BTreeMap::new(),
|
||||
exit_symbols: BTreeSet::new(),
|
||||
@@ -88,10 +105,78 @@ impl Strategy for BuyAndHoldStrategy {
|
||||
},
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn stock_market_snapshot(date: NaiveDate) -> DailyMarketSnapshot {
|
||||
DailyMarketSnapshot {
|
||||
date,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
timestamp: Some(format!("{date} 10:18:00")),
|
||||
day_open: 10.0,
|
||||
open: 10.0,
|
||||
high: 10.1,
|
||||
low: 9.9,
|
||||
close: 10.0,
|
||||
last_price: 10.0,
|
||||
bid1: 10.0,
|
||||
ask1: 10.0,
|
||||
prev_close: 10.0,
|
||||
volume: 100_000,
|
||||
minute_volume: 100_000,
|
||||
bid1_volume: 100_000,
|
||||
ask1_volume: 100_000,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
paused: false,
|
||||
upper_limit: 11.0,
|
||||
lower_limit: 9.0,
|
||||
price_tick: 0.01,
|
||||
}
|
||||
}
|
||||
|
||||
fn stock_factor_snapshot(date: NaiveDate) -> DailyFactorSnapshot {
|
||||
DailyFactorSnapshot {
|
||||
date,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
market_cap_bn: 20.0,
|
||||
free_float_cap_bn: 18.0,
|
||||
pe_ttm: 10.0,
|
||||
turnover_ratio: Some(1.0),
|
||||
effective_turnover_ratio: Some(1.0),
|
||||
adjustment_factor_backward1: None,
|
||||
extra_factors: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn stock_candidate(date: NaiveDate) -> CandidateEligibility {
|
||||
CandidateEligibility {
|
||||
date,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
is_paused: false,
|
||||
allow_buy: true,
|
||||
allow_sell: true,
|
||||
is_kcb: false,
|
||||
is_one_yuan: false,
|
||||
risk_level_code: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn benchmark_snapshot(date: NaiveDate) -> BenchmarkSnapshot {
|
||||
BenchmarkSnapshot {
|
||||
date,
|
||||
benchmark: "000300.SH".to_string(),
|
||||
open: 100.0,
|
||||
close: 100.0,
|
||||
prev_close: 99.0,
|
||||
volume: 1_000_000,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn engine_reinvests_dividend_receivable_in_round_lots() {
|
||||
let buy_date = d(2025, 1, 1);
|
||||
@@ -122,7 +207,7 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
|
||||
ask1: 10.0,
|
||||
prev_close: 10.0,
|
||||
volume: 100_000,
|
||||
tick_volume: 100_000,
|
||||
minute_volume: 100_000,
|
||||
bid1_volume: 100_000,
|
||||
ask1_volume: 100_000,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
@@ -145,7 +230,7 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
|
||||
ask1: 10.0,
|
||||
prev_close: 10.0,
|
||||
volume: 100_000,
|
||||
tick_volume: 100_000,
|
||||
minute_volume: 100_000,
|
||||
bid1_volume: 100_000,
|
||||
ask1_volume: 100_000,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
@@ -168,7 +253,7 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
|
||||
ask1: 10.0,
|
||||
prev_close: 10.0,
|
||||
volume: 100_000,
|
||||
tick_volume: 100_000,
|
||||
minute_volume: 100_000,
|
||||
bid1_volume: 100_000,
|
||||
ask1_volume: 100_000,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
@@ -187,6 +272,7 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
|
||||
pe_ttm: 10.0,
|
||||
turnover_ratio: Some(1.0),
|
||||
effective_turnover_ratio: Some(1.0),
|
||||
adjustment_factor_backward1: None,
|
||||
extra_factors: BTreeMap::new(),
|
||||
},
|
||||
DailyFactorSnapshot {
|
||||
@@ -197,6 +283,7 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
|
||||
pe_ttm: 10.0,
|
||||
turnover_ratio: Some(1.0),
|
||||
effective_turnover_ratio: Some(1.0),
|
||||
adjustment_factor_backward1: None,
|
||||
extra_factors: BTreeMap::new(),
|
||||
},
|
||||
DailyFactorSnapshot {
|
||||
@@ -207,6 +294,7 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
|
||||
pe_ttm: 10.0,
|
||||
turnover_ratio: Some(1.0),
|
||||
effective_turnover_ratio: Some(1.0),
|
||||
adjustment_factor_backward1: None,
|
||||
extra_factors: BTreeMap::new(),
|
||||
},
|
||||
],
|
||||
@@ -215,34 +303,40 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
|
||||
date: buy_date,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
is_paused: false,
|
||||
allow_buy: true,
|
||||
allow_sell: true,
|
||||
is_kcb: false,
|
||||
is_one_yuan: false,
|
||||
risk_level_code: None,
|
||||
},
|
||||
CandidateEligibility {
|
||||
date: ex_date,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
is_paused: false,
|
||||
allow_buy: true,
|
||||
allow_sell: true,
|
||||
is_kcb: false,
|
||||
is_one_yuan: false,
|
||||
risk_level_code: None,
|
||||
},
|
||||
CandidateEligibility {
|
||||
date: payable_date,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
is_paused: false,
|
||||
allow_buy: true,
|
||||
allow_sell: true,
|
||||
is_kcb: false,
|
||||
is_one_yuan: false,
|
||||
risk_level_code: None,
|
||||
},
|
||||
],
|
||||
vec![
|
||||
@@ -295,12 +389,14 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
|
||||
first_date: buy_date,
|
||||
},
|
||||
BrokerSimulator::new_with_execution_price(
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaAShareCostModel::default()
|
||||
.with_commission_rate(0.0008)
|
||||
.with_minimum_commission(0.0),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
),
|
||||
BacktestConfig {
|
||||
initial_cash: 11_005.0,
|
||||
initial_cash: 11_008.0,
|
||||
benchmark_code: "000300.SH".to_string(),
|
||||
start_date: Some(buy_date),
|
||||
end_date: Some(payable_date),
|
||||
@@ -327,3 +423,96 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
|
||||
assert_eq!(reinvest_fill.commission, 0.0);
|
||||
assert_eq!(reinvest_fill.stamp_tax, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn engine_settles_same_day_dividend_after_split_for_aiquant_semantics() {
|
||||
let buy_date = d(2025, 1, 1);
|
||||
let ex_date = d(2025, 1, 2);
|
||||
let data = DataSet::from_components_with_actions(
|
||||
vec![Instrument {
|
||||
symbol: "000001.SZ".to_string(),
|
||||
name: "Anchor".to_string(),
|
||||
board: "SZ".to_string(),
|
||||
round_lot: 100,
|
||||
listed_at: Some(d(2020, 1, 1)),
|
||||
delisted_at: None,
|
||||
status: "active".to_string(),
|
||||
}],
|
||||
vec![
|
||||
stock_market_snapshot(buy_date),
|
||||
stock_market_snapshot(ex_date),
|
||||
],
|
||||
vec![
|
||||
stock_factor_snapshot(buy_date),
|
||||
stock_factor_snapshot(ex_date),
|
||||
],
|
||||
vec![stock_candidate(buy_date), stock_candidate(ex_date)],
|
||||
vec![benchmark_snapshot(buy_date), benchmark_snapshot(ex_date)],
|
||||
vec![CorporateAction {
|
||||
date: ex_date,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
payable_date: Some(ex_date),
|
||||
share_cash: 1.05,
|
||||
share_bonus: 0.2,
|
||||
share_gift: 0.0,
|
||||
issue_quantity: 0.0,
|
||||
issue_price: 0.0,
|
||||
reform: false,
|
||||
adjust_factor: None,
|
||||
successor_symbol: None,
|
||||
successor_ratio: None,
|
||||
successor_cash: None,
|
||||
}],
|
||||
)
|
||||
.expect("dataset");
|
||||
|
||||
let mut engine = BacktestEngine::new(
|
||||
data,
|
||||
BuyAndHoldStrategy {
|
||||
first_date: buy_date,
|
||||
},
|
||||
BrokerSimulator::new_with_execution_price(
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
),
|
||||
BacktestConfig {
|
||||
initial_cash: 11_008.0,
|
||||
benchmark_code: "000300.SH".to_string(),
|
||||
start_date: Some(buy_date),
|
||||
end_date: Some(ex_date),
|
||||
decision_lag_trading_days: 0,
|
||||
execution_price_field: PriceField::Open,
|
||||
},
|
||||
)
|
||||
.with_dividend_reinvestment(true);
|
||||
|
||||
let result = engine.run().expect("backtest run");
|
||||
let final_holding = result
|
||||
.holdings_summary
|
||||
.iter()
|
||||
.find(|row| row.symbol == "000001.SZ")
|
||||
.expect("holding");
|
||||
assert_eq!(final_holding.quantity, 1_300);
|
||||
|
||||
let reinvest_fill = result
|
||||
.fills
|
||||
.iter()
|
||||
.find(|fill| fill.reason == "dividend_reinvestment")
|
||||
.expect("reinvestment fill");
|
||||
assert_eq!(reinvest_fill.quantity, 100);
|
||||
assert!((reinvest_fill.price - ((10.0 - 1.05) / 1.2)).abs() < 1e-9);
|
||||
|
||||
assert!(
|
||||
result
|
||||
.position_events
|
||||
.iter()
|
||||
.any(|event| event.reason == "stock_split 1.200000" && event.quantity_after == 1_200)
|
||||
);
|
||||
assert!(
|
||||
result
|
||||
.account_events
|
||||
.iter()
|
||||
.any(|event| event.note.contains("cash_receivable_reinvested"))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,844 @@
|
||||
use chrono::{Duration, NaiveDate, NaiveTime};
|
||||
use fidc_core::{
|
||||
BacktestConfig, BacktestEngine, BenchmarkSnapshot, BrokerSimulator, CandidateEligibility,
|
||||
ChinaAShareCostModel, ChinaEquityRuleHooks, DailyFactorSnapshot, DailyMarketSnapshot, DataSet,
|
||||
IntradayExecutionQuote, MatchingType, OrderIntent, PriceField, Strategy, StrategyContext,
|
||||
StrategyDecision,
|
||||
};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
fn d(year: i32, month: u32, day: u32) -> NaiveDate {
|
||||
NaiveDate::from_ymd_opt(year, month, day).expect("valid date")
|
||||
}
|
||||
|
||||
fn t(hour: u32, minute: u32, second: u32) -> NaiveTime {
|
||||
NaiveTime::from_hms_opt(hour, minute, second).expect("valid time")
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct DecisionQuoteReader {
|
||||
day_count: usize,
|
||||
}
|
||||
|
||||
impl Strategy for DecisionQuoteReader {
|
||||
fn name(&self) -> &str {
|
||||
"decision_quote_reader"
|
||||
}
|
||||
|
||||
fn decision_quote_times(&self) -> Vec<NaiveTime> {
|
||||
vec![t(10, 40, 0)]
|
||||
}
|
||||
|
||||
fn on_day(
|
||||
&mut self,
|
||||
ctx: &StrategyContext<'_>,
|
||||
) -> Result<StrategyDecision, fidc_core::BacktestError> {
|
||||
self.day_count += 1;
|
||||
if self.day_count == 1 {
|
||||
return Ok(StrategyDecision {
|
||||
order_intents: vec![OrderIntent::Value {
|
||||
symbol: "000001.SZ".to_string(),
|
||||
value: 5_000.0,
|
||||
reason: "seed_position".to_string(),
|
||||
}],
|
||||
..StrategyDecision::default()
|
||||
});
|
||||
}
|
||||
|
||||
assert!(
|
||||
ctx.portfolio.position("000001.SZ").is_some(),
|
||||
"second day should carry the first day position"
|
||||
);
|
||||
let quote_loaded_before_decision = ctx
|
||||
.data
|
||||
.execution_quotes_on(ctx.execution_date, "000001.SZ")
|
||||
.iter()
|
||||
.any(|quote| quote.timestamp.time() == t(10, 39, 59) && quote.last_price == 11.0);
|
||||
assert!(
|
||||
quote_loaded_before_decision,
|
||||
"engine must load declared decision quote before strategy.on_day"
|
||||
);
|
||||
Ok(StrategyDecision::default())
|
||||
}
|
||||
}
|
||||
|
||||
struct NoLoaderDecisionQuoteStrategy {
|
||||
symbol_plan_calls: Arc<Mutex<usize>>,
|
||||
}
|
||||
|
||||
impl Strategy for NoLoaderDecisionQuoteStrategy {
|
||||
fn name(&self) -> &str {
|
||||
"no_loader_decision_quote_strategy"
|
||||
}
|
||||
|
||||
fn decision_quote_times(&self) -> Vec<NaiveTime> {
|
||||
vec![t(10, 18, 0)]
|
||||
}
|
||||
|
||||
fn decision_quote_symbols(
|
||||
&mut self,
|
||||
_ctx: &StrategyContext<'_>,
|
||||
) -> Result<BTreeSet<String>, fidc_core::BacktestError> {
|
||||
*self
|
||||
.symbol_plan_calls
|
||||
.lock()
|
||||
.expect("symbol plan counter mutex") += 1;
|
||||
Ok(BTreeSet::new())
|
||||
}
|
||||
}
|
||||
|
||||
fn single_day_quote_plan_data(date: NaiveDate) -> DataSet {
|
||||
DataSet::from_components(
|
||||
Vec::new(),
|
||||
vec![DailyMarketSnapshot {
|
||||
date,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
timestamp: Some(format!("{date} 15:00:00")),
|
||||
day_open: 10.0,
|
||||
open: 10.0,
|
||||
high: 10.2,
|
||||
low: 9.9,
|
||||
close: 10.0,
|
||||
last_price: 10.0,
|
||||
bid1: 10.0,
|
||||
ask1: 10.0,
|
||||
prev_close: 9.8,
|
||||
volume: 10_000,
|
||||
minute_volume: 1_000,
|
||||
bid1_volume: 10_000,
|
||||
ask1_volume: 10_000,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
paused: false,
|
||||
upper_limit: 10.78,
|
||||
lower_limit: 8.82,
|
||||
price_tick: 0.01,
|
||||
}],
|
||||
vec![DailyFactorSnapshot {
|
||||
date,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
market_cap_bn: 10.0,
|
||||
free_float_cap_bn: 10.0,
|
||||
pe_ttm: 10.0,
|
||||
turnover_ratio: None,
|
||||
effective_turnover_ratio: None,
|
||||
adjustment_factor_backward1: None,
|
||||
extra_factors: Default::default(),
|
||||
}],
|
||||
vec![CandidateEligibility {
|
||||
date,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
is_paused: false,
|
||||
allow_buy: true,
|
||||
allow_sell: true,
|
||||
is_kcb: false,
|
||||
is_one_yuan: false,
|
||||
risk_level_code: None,
|
||||
}],
|
||||
vec![BenchmarkSnapshot {
|
||||
date,
|
||||
benchmark: "000852.SH".to_string(),
|
||||
open: 1000.0,
|
||||
close: 1001.0,
|
||||
prev_close: 999.0,
|
||||
volume: 1_000_000,
|
||||
}],
|
||||
)
|
||||
.expect("dataset")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn engine_uses_preplanned_decision_symbols_without_recomputing_strategy_plan() {
|
||||
let date = d(2026, 1, 5);
|
||||
let data = single_day_quote_plan_data(date);
|
||||
let broker = BrokerSimulator::new_with_execution_price(
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks,
|
||||
PriceField::Close,
|
||||
)
|
||||
.with_matching_type(MatchingType::CurrentBarClose);
|
||||
let config = BacktestConfig {
|
||||
initial_cash: 10_000.0,
|
||||
benchmark_code: "000852.SH".to_string(),
|
||||
start_date: Some(date),
|
||||
end_date: Some(date),
|
||||
decision_lag_trading_days: 0,
|
||||
execution_price_field: PriceField::Close,
|
||||
};
|
||||
let symbol_plan_calls = Arc::new(Mutex::new(0usize));
|
||||
let loader_calls = Arc::new(Mutex::new(0usize));
|
||||
let strategy = NoLoaderDecisionQuoteStrategy {
|
||||
symbol_plan_calls: Arc::clone(&symbol_plan_calls),
|
||||
};
|
||||
let captured_loader_calls = Arc::clone(&loader_calls);
|
||||
let preplanned = Arc::new(BTreeMap::from([(
|
||||
date,
|
||||
BTreeSet::from(["000001.SZ".to_string()]),
|
||||
)]));
|
||||
let mut engine = BacktestEngine::new(data, strategy, broker, config)
|
||||
.with_execution_quote_loader(move |request| {
|
||||
*captured_loader_calls.lock().expect("loader counter mutex") += 1;
|
||||
Ok(request
|
||||
.symbols
|
||||
.into_iter()
|
||||
.map(|symbol| IntradayExecutionQuote {
|
||||
date: request.date,
|
||||
symbol,
|
||||
timestamp: request.date.and_time(t(10, 17, 59)),
|
||||
last_price: 10.0,
|
||||
bid1: 10.0,
|
||||
ask1: 10.0,
|
||||
bid1_volume: 10_000,
|
||||
ask1_volume: 10_000,
|
||||
volume_delta: 10_000,
|
||||
amount_delta: 100_000.0,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
})
|
||||
.collect())
|
||||
})
|
||||
.with_preplanned_decision_quote_symbols_by_date(preplanned);
|
||||
|
||||
engine.run().expect("backtest should run");
|
||||
|
||||
assert_eq!(
|
||||
*symbol_plan_calls.lock().expect("symbol plan counter mutex"),
|
||||
0,
|
||||
"the strategy plan must not be recomputed after a complete plan is supplied"
|
||||
);
|
||||
assert_eq!(
|
||||
*loader_calls.lock().expect("loader counter mutex"),
|
||||
1,
|
||||
"the supplied symbols must still pass through the normal quote loader"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn engine_skips_decision_quote_symbol_plan_without_loader() {
|
||||
let date = d(2026, 1, 5);
|
||||
let data = single_day_quote_plan_data(date);
|
||||
let broker = BrokerSimulator::new_with_execution_price(
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks,
|
||||
PriceField::Close,
|
||||
)
|
||||
.with_matching_type(MatchingType::CurrentBarClose);
|
||||
let config = BacktestConfig {
|
||||
initial_cash: 10_000.0,
|
||||
benchmark_code: "000852.SH".to_string(),
|
||||
start_date: Some(date),
|
||||
end_date: Some(date),
|
||||
decision_lag_trading_days: 0,
|
||||
execution_price_field: PriceField::Close,
|
||||
};
|
||||
let symbol_plan_calls = Arc::new(Mutex::new(0usize));
|
||||
let strategy = NoLoaderDecisionQuoteStrategy {
|
||||
symbol_plan_calls: Arc::clone(&symbol_plan_calls),
|
||||
};
|
||||
let mut engine = BacktestEngine::new(data, strategy, broker, config);
|
||||
|
||||
engine.run().expect("backtest should run");
|
||||
|
||||
assert_eq!(
|
||||
*symbol_plan_calls.lock().expect("symbol plan counter mutex"),
|
||||
0,
|
||||
"a preloaded/no-loader engine cannot use a newly computed quote symbol plan"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn engine_preloads_declared_decision_quotes_for_current_positions() {
|
||||
let first = d(2026, 1, 5);
|
||||
let second = d(2026, 1, 6);
|
||||
let data = DataSet::from_components(
|
||||
Vec::new(),
|
||||
vec![
|
||||
DailyMarketSnapshot {
|
||||
date: first,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
timestamp: Some("2026-01-05 15:00:00".to_string()),
|
||||
day_open: 10.0,
|
||||
open: 10.0,
|
||||
high: 10.2,
|
||||
low: 9.9,
|
||||
close: 10.0,
|
||||
last_price: 10.0,
|
||||
bid1: 10.0,
|
||||
ask1: 10.0,
|
||||
prev_close: 9.8,
|
||||
volume: 10_000,
|
||||
minute_volume: 1_000,
|
||||
bid1_volume: 10_000,
|
||||
ask1_volume: 10_000,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
paused: false,
|
||||
upper_limit: 10.78,
|
||||
lower_limit: 8.82,
|
||||
price_tick: 0.01,
|
||||
},
|
||||
DailyMarketSnapshot {
|
||||
date: second,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
timestamp: Some("2026-01-06 15:00:00".to_string()),
|
||||
day_open: 10.5,
|
||||
open: 10.5,
|
||||
high: 11.2,
|
||||
low: 10.4,
|
||||
close: 10.6,
|
||||
last_price: 10.6,
|
||||
bid1: 10.6,
|
||||
ask1: 10.6,
|
||||
prev_close: 10.0,
|
||||
volume: 10_000,
|
||||
minute_volume: 1_000,
|
||||
bid1_volume: 10_000,
|
||||
ask1_volume: 10_000,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
paused: false,
|
||||
upper_limit: 11.0,
|
||||
lower_limit: 9.0,
|
||||
price_tick: 0.01,
|
||||
},
|
||||
],
|
||||
vec![
|
||||
DailyFactorSnapshot {
|
||||
date: first,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
market_cap_bn: 10.0,
|
||||
free_float_cap_bn: 10.0,
|
||||
pe_ttm: 10.0,
|
||||
turnover_ratio: None,
|
||||
effective_turnover_ratio: None,
|
||||
adjustment_factor_backward1: None,
|
||||
extra_factors: Default::default(),
|
||||
},
|
||||
DailyFactorSnapshot {
|
||||
date: second,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
market_cap_bn: 10.0,
|
||||
free_float_cap_bn: 10.0,
|
||||
pe_ttm: 10.0,
|
||||
turnover_ratio: None,
|
||||
effective_turnover_ratio: None,
|
||||
adjustment_factor_backward1: None,
|
||||
extra_factors: Default::default(),
|
||||
},
|
||||
],
|
||||
vec![
|
||||
CandidateEligibility {
|
||||
date: first,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
is_paused: false,
|
||||
allow_buy: true,
|
||||
allow_sell: true,
|
||||
is_kcb: false,
|
||||
is_one_yuan: false,
|
||||
risk_level_code: None,
|
||||
},
|
||||
CandidateEligibility {
|
||||
date: second,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
is_paused: false,
|
||||
allow_buy: true,
|
||||
allow_sell: true,
|
||||
is_kcb: false,
|
||||
is_one_yuan: false,
|
||||
risk_level_code: None,
|
||||
},
|
||||
],
|
||||
vec![
|
||||
BenchmarkSnapshot {
|
||||
date: first,
|
||||
benchmark: "000852.SH".to_string(),
|
||||
open: 1000.0,
|
||||
close: 1000.0,
|
||||
prev_close: 990.0,
|
||||
volume: 1_000_000,
|
||||
},
|
||||
BenchmarkSnapshot {
|
||||
date: second,
|
||||
benchmark: "000852.SH".to_string(),
|
||||
open: 1000.0,
|
||||
close: 1001.0,
|
||||
prev_close: 1000.0,
|
||||
volume: 1_000_000,
|
||||
},
|
||||
],
|
||||
)
|
||||
.expect("dataset");
|
||||
|
||||
let broker = BrokerSimulator::new_with_execution_price(
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks,
|
||||
PriceField::Last,
|
||||
)
|
||||
.with_matching_type(MatchingType::MinuteLast)
|
||||
.with_intraday_execution_start_time(t(10, 40, 0));
|
||||
let config = BacktestConfig {
|
||||
initial_cash: 10_000.0,
|
||||
benchmark_code: "000852.SH".to_string(),
|
||||
start_date: Some(first),
|
||||
end_date: Some(second),
|
||||
decision_lag_trading_days: 0,
|
||||
execution_price_field: PriceField::Last,
|
||||
};
|
||||
let mut engine = BacktestEngine::new(data, DecisionQuoteReader::default(), broker, config)
|
||||
.with_execution_quote_loader(move |request| {
|
||||
assert_eq!(
|
||||
request.end_time, None,
|
||||
"decision quote preload must request latest quote at or before start_time"
|
||||
);
|
||||
Ok(request
|
||||
.symbols
|
||||
.into_iter()
|
||||
.map(|symbol| IntradayExecutionQuote {
|
||||
date: request.date,
|
||||
symbol,
|
||||
timestamp: request.date.and_time(t(10, 39, 59)),
|
||||
last_price: if request.date == second { 11.0 } else { 10.0 },
|
||||
bid1: if request.date == second { 11.0 } else { 10.0 },
|
||||
ask1: if request.date == second { 11.0 } else { 10.0 },
|
||||
bid1_volume: 10_000,
|
||||
ask1_volume: 10_000,
|
||||
volume_delta: 10_000,
|
||||
amount_delta: 100_000.0,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
})
|
||||
.collect())
|
||||
});
|
||||
|
||||
engine.run().expect("backtest should run");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
|
||||
let first = d(2026, 1, 5);
|
||||
let second = d(2026, 1, 6);
|
||||
let data = DataSet::from_components_with_actions_and_quotes(
|
||||
Vec::new(),
|
||||
vec![
|
||||
DailyMarketSnapshot {
|
||||
date: first,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
timestamp: Some("2026-01-05 15:00:00".to_string()),
|
||||
day_open: 10.0,
|
||||
open: 10.0,
|
||||
high: 10.2,
|
||||
low: 9.9,
|
||||
close: 10.0,
|
||||
last_price: 10.0,
|
||||
bid1: 10.0,
|
||||
ask1: 10.0,
|
||||
prev_close: 9.8,
|
||||
volume: 10_000,
|
||||
minute_volume: 1_000,
|
||||
bid1_volume: 10_000,
|
||||
ask1_volume: 10_000,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
paused: false,
|
||||
upper_limit: 10.78,
|
||||
lower_limit: 8.82,
|
||||
price_tick: 0.01,
|
||||
},
|
||||
DailyMarketSnapshot {
|
||||
date: second,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
timestamp: Some("2026-01-06 15:00:00".to_string()),
|
||||
day_open: 10.5,
|
||||
open: 10.5,
|
||||
high: 11.2,
|
||||
low: 10.4,
|
||||
close: 10.6,
|
||||
last_price: 10.6,
|
||||
bid1: 10.6,
|
||||
ask1: 10.6,
|
||||
prev_close: 10.0,
|
||||
volume: 10_000,
|
||||
minute_volume: 1_000,
|
||||
bid1_volume: 10_000,
|
||||
ask1_volume: 10_000,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
paused: false,
|
||||
upper_limit: 11.0,
|
||||
lower_limit: 9.0,
|
||||
price_tick: 0.01,
|
||||
},
|
||||
],
|
||||
vec![
|
||||
DailyFactorSnapshot {
|
||||
date: first,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
market_cap_bn: 10.0,
|
||||
free_float_cap_bn: 10.0,
|
||||
pe_ttm: 10.0,
|
||||
turnover_ratio: None,
|
||||
effective_turnover_ratio: None,
|
||||
adjustment_factor_backward1: None,
|
||||
extra_factors: Default::default(),
|
||||
},
|
||||
DailyFactorSnapshot {
|
||||
date: second,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
market_cap_bn: 10.0,
|
||||
free_float_cap_bn: 10.0,
|
||||
pe_ttm: 10.0,
|
||||
turnover_ratio: None,
|
||||
effective_turnover_ratio: None,
|
||||
adjustment_factor_backward1: None,
|
||||
extra_factors: Default::default(),
|
||||
},
|
||||
],
|
||||
vec![
|
||||
CandidateEligibility {
|
||||
date: first,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
is_paused: false,
|
||||
allow_buy: true,
|
||||
allow_sell: true,
|
||||
is_kcb: false,
|
||||
is_one_yuan: false,
|
||||
risk_level_code: None,
|
||||
},
|
||||
CandidateEligibility {
|
||||
date: second,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
is_paused: false,
|
||||
allow_buy: true,
|
||||
allow_sell: true,
|
||||
is_kcb: false,
|
||||
is_one_yuan: false,
|
||||
risk_level_code: None,
|
||||
},
|
||||
],
|
||||
vec![
|
||||
BenchmarkSnapshot {
|
||||
date: first,
|
||||
benchmark: "000852.SH".to_string(),
|
||||
open: 1000.0,
|
||||
close: 1000.0,
|
||||
prev_close: 990.0,
|
||||
volume: 1_000_000,
|
||||
},
|
||||
BenchmarkSnapshot {
|
||||
date: second,
|
||||
benchmark: "000852.SH".to_string(),
|
||||
open: 1000.0,
|
||||
close: 1001.0,
|
||||
prev_close: 1000.0,
|
||||
volume: 1_000_000,
|
||||
},
|
||||
],
|
||||
Vec::new(),
|
||||
vec![
|
||||
IntradayExecutionQuote {
|
||||
date: first,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
timestamp: first.and_time(t(10, 39, 59)),
|
||||
last_price: 10.0,
|
||||
bid1: 10.0,
|
||||
ask1: 10.0,
|
||||
bid1_volume: 10_000,
|
||||
ask1_volume: 10_000,
|
||||
volume_delta: 10_000,
|
||||
amount_delta: 100_000.0,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
},
|
||||
IntradayExecutionQuote {
|
||||
date: second,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
timestamp: second.and_time(t(10, 39, 59)),
|
||||
last_price: 11.0,
|
||||
bid1: 11.0,
|
||||
ask1: 11.0,
|
||||
bid1_volume: 10_000,
|
||||
ask1_volume: 10_000,
|
||||
volume_delta: 10_000,
|
||||
amount_delta: 100_000.0,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
},
|
||||
],
|
||||
)
|
||||
.expect("dataset");
|
||||
|
||||
let broker = BrokerSimulator::new_with_execution_price(
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks,
|
||||
PriceField::Last,
|
||||
)
|
||||
.with_matching_type(MatchingType::MinuteLast)
|
||||
.with_intraday_execution_start_time(t(10, 40, 0));
|
||||
let config = BacktestConfig {
|
||||
initial_cash: 10_000.0,
|
||||
benchmark_code: "000852.SH".to_string(),
|
||||
start_date: Some(first),
|
||||
end_date: Some(second),
|
||||
decision_lag_trading_days: 0,
|
||||
execution_price_field: PriceField::Last,
|
||||
};
|
||||
let loader_calls = Arc::new(Mutex::new(0usize));
|
||||
let captured_loader_calls = Arc::clone(&loader_calls);
|
||||
let mut engine = BacktestEngine::new(data, DecisionQuoteReader::default(), broker, config)
|
||||
.with_execution_quote_loader(move |_| {
|
||||
*captured_loader_calls.lock().expect("loader mutex") += 1;
|
||||
Ok(Vec::new())
|
||||
});
|
||||
|
||||
engine.run().expect("backtest should run");
|
||||
assert_eq!(
|
||||
*loader_calls.lock().expect("loader mutex"),
|
||||
0,
|
||||
"preloaded execution quotes should satisfy decision-time quote requests"
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct MultiTimeDecisionQuoteReader {
|
||||
day_count: usize,
|
||||
}
|
||||
|
||||
impl Strategy for MultiTimeDecisionQuoteReader {
|
||||
fn name(&self) -> &str {
|
||||
"multi_time_decision_quote_reader"
|
||||
}
|
||||
|
||||
fn decision_quote_times(&self) -> Vec<NaiveTime> {
|
||||
vec![t(10, 31, 0), t(10, 40, 0)]
|
||||
}
|
||||
|
||||
fn on_day(
|
||||
&mut self,
|
||||
ctx: &StrategyContext<'_>,
|
||||
) -> Result<StrategyDecision, fidc_core::BacktestError> {
|
||||
self.day_count += 1;
|
||||
if self.day_count == 1 {
|
||||
return Ok(StrategyDecision {
|
||||
order_intents: vec![OrderIntent::Value {
|
||||
symbol: "000001.SZ".to_string(),
|
||||
value: 5_000.0,
|
||||
reason: "seed_position".to_string(),
|
||||
}],
|
||||
..StrategyDecision::default()
|
||||
});
|
||||
}
|
||||
|
||||
let quote_times = ctx
|
||||
.data
|
||||
.execution_quotes_on(ctx.execution_date, "000001.SZ")
|
||||
.iter()
|
||||
.map(|quote| quote.timestamp.time())
|
||||
.collect::<Vec<_>>();
|
||||
assert!(
|
||||
quote_times.contains(&t(10, 30, 59)),
|
||||
"10:31 decision quote must be loaded"
|
||||
);
|
||||
assert!(
|
||||
quote_times.contains(&t(10, 39, 59)),
|
||||
"10:40 decision quote must not be skipped because 10:31 was loaded"
|
||||
);
|
||||
Ok(StrategyDecision::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn engine_loads_distinct_decision_quote_times_on_same_day() {
|
||||
let first = d(2026, 1, 5);
|
||||
let second = d(2026, 1, 6);
|
||||
let data = DataSet::from_components(
|
||||
Vec::new(),
|
||||
vec![
|
||||
DailyMarketSnapshot {
|
||||
date: first,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
timestamp: Some("2026-01-05 15:00:00".to_string()),
|
||||
day_open: 10.0,
|
||||
open: 10.0,
|
||||
high: 10.2,
|
||||
low: 9.9,
|
||||
close: 10.0,
|
||||
last_price: 10.0,
|
||||
bid1: 10.0,
|
||||
ask1: 10.0,
|
||||
prev_close: 9.8,
|
||||
volume: 10_000,
|
||||
minute_volume: 1_000,
|
||||
bid1_volume: 10_000,
|
||||
ask1_volume: 10_000,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
paused: false,
|
||||
upper_limit: 10.78,
|
||||
lower_limit: 8.82,
|
||||
price_tick: 0.01,
|
||||
},
|
||||
DailyMarketSnapshot {
|
||||
date: second,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
timestamp: Some("2026-01-06 15:00:00".to_string()),
|
||||
day_open: 10.5,
|
||||
open: 10.5,
|
||||
high: 11.2,
|
||||
low: 10.4,
|
||||
close: 10.6,
|
||||
last_price: 10.6,
|
||||
bid1: 10.6,
|
||||
ask1: 10.6,
|
||||
prev_close: 10.0,
|
||||
volume: 10_000,
|
||||
minute_volume: 1_000,
|
||||
bid1_volume: 10_000,
|
||||
ask1_volume: 10_000,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
paused: false,
|
||||
upper_limit: 11.0,
|
||||
lower_limit: 9.0,
|
||||
price_tick: 0.01,
|
||||
},
|
||||
],
|
||||
vec![
|
||||
DailyFactorSnapshot {
|
||||
date: first,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
market_cap_bn: 10.0,
|
||||
free_float_cap_bn: 10.0,
|
||||
pe_ttm: 10.0,
|
||||
turnover_ratio: None,
|
||||
effective_turnover_ratio: None,
|
||||
adjustment_factor_backward1: None,
|
||||
extra_factors: Default::default(),
|
||||
},
|
||||
DailyFactorSnapshot {
|
||||
date: second,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
market_cap_bn: 10.0,
|
||||
free_float_cap_bn: 10.0,
|
||||
pe_ttm: 10.0,
|
||||
turnover_ratio: None,
|
||||
effective_turnover_ratio: None,
|
||||
adjustment_factor_backward1: None,
|
||||
extra_factors: Default::default(),
|
||||
},
|
||||
],
|
||||
vec![
|
||||
CandidateEligibility {
|
||||
date: first,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
is_paused: false,
|
||||
allow_buy: true,
|
||||
allow_sell: true,
|
||||
is_kcb: false,
|
||||
is_one_yuan: false,
|
||||
risk_level_code: None,
|
||||
},
|
||||
CandidateEligibility {
|
||||
date: second,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
is_paused: false,
|
||||
allow_buy: true,
|
||||
allow_sell: true,
|
||||
is_kcb: false,
|
||||
is_one_yuan: false,
|
||||
risk_level_code: None,
|
||||
},
|
||||
],
|
||||
vec![
|
||||
BenchmarkSnapshot {
|
||||
date: first,
|
||||
benchmark: "000852.SH".to_string(),
|
||||
open: 1000.0,
|
||||
close: 1000.0,
|
||||
prev_close: 990.0,
|
||||
volume: 1_000_000,
|
||||
},
|
||||
BenchmarkSnapshot {
|
||||
date: second,
|
||||
benchmark: "000852.SH".to_string(),
|
||||
open: 1000.0,
|
||||
close: 1001.0,
|
||||
prev_close: 1000.0,
|
||||
volume: 1_000_000,
|
||||
},
|
||||
],
|
||||
)
|
||||
.expect("dataset");
|
||||
|
||||
let broker = BrokerSimulator::new_with_execution_price(
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks,
|
||||
PriceField::Last,
|
||||
)
|
||||
.with_matching_type(MatchingType::MinuteLast)
|
||||
.with_intraday_execution_start_time(t(10, 40, 0));
|
||||
let config = BacktestConfig {
|
||||
initial_cash: 10_000.0,
|
||||
benchmark_code: "000852.SH".to_string(),
|
||||
start_date: Some(first),
|
||||
end_date: Some(second),
|
||||
decision_lag_trading_days: 0,
|
||||
execution_price_field: PriceField::Last,
|
||||
};
|
||||
let requests = Arc::new(Mutex::new(Vec::<(NaiveDate, NaiveTime)>::new()));
|
||||
let captured_requests = Arc::clone(&requests);
|
||||
let mut engine = BacktestEngine::new(
|
||||
data,
|
||||
MultiTimeDecisionQuoteReader::default(),
|
||||
broker,
|
||||
config,
|
||||
)
|
||||
.with_execution_quote_loader(move |request| {
|
||||
let start_time = request
|
||||
.start_time
|
||||
.expect("decision quote loader request must include start_time");
|
||||
captured_requests
|
||||
.lock()
|
||||
.expect("request mutex")
|
||||
.push((request.date, start_time));
|
||||
Ok(request
|
||||
.symbols
|
||||
.into_iter()
|
||||
.map(|symbol| IntradayExecutionQuote {
|
||||
date: request.date,
|
||||
symbol,
|
||||
timestamp: request.date.and_time(start_time) - Duration::seconds(1),
|
||||
last_price: 10.0,
|
||||
bid1: 10.0,
|
||||
ask1: 10.0,
|
||||
bid1_volume: 10_000,
|
||||
ask1_volume: 10_000,
|
||||
volume_delta: 10_000,
|
||||
amount_delta: 100_000.0,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
})
|
||||
.collect())
|
||||
});
|
||||
|
||||
engine.run().expect("backtest should run");
|
||||
|
||||
let requests = requests.lock().expect("request mutex").clone();
|
||||
assert!(
|
||||
requests.contains(&(second, t(10, 31, 0))),
|
||||
"second-day 10:31 quote request is required"
|
||||
);
|
||||
assert!(
|
||||
requests.contains(&(second, t(10, 40, 0))),
|
||||
"second-day 10:40 quote request must not be skipped by earlier quote"
|
||||
);
|
||||
}
|
||||
@@ -24,6 +24,7 @@ impl Strategy for BuyThenHoldStrategy {
|
||||
) -> Result<StrategyDecision, fidc_core::BacktestError> {
|
||||
if ctx.decision_date == d(2025, 1, 2) && ctx.portfolio.position("000001.SZ").is_none() {
|
||||
return Ok(StrategyDecision {
|
||||
buy_denials: Default::default(),
|
||||
rebalance: false,
|
||||
target_weights: BTreeMap::new(),
|
||||
exit_symbols: BTreeSet::new(),
|
||||
@@ -34,6 +35,7 @@ impl Strategy for BuyThenHoldStrategy {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
});
|
||||
}
|
||||
Ok(StrategyDecision::default())
|
||||
@@ -41,9 +43,10 @@ impl Strategy for BuyThenHoldStrategy {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn engine_settles_delisted_position_before_missing_market_snapshot_breaks_run() {
|
||||
fn engine_keeps_unresolved_delisted_position_without_fabricating_a_fill() {
|
||||
let date1 = d(2025, 1, 2);
|
||||
let date2 = d(2025, 1, 3);
|
||||
let delist_date = d(2025, 1, 3);
|
||||
let date2 = d(2025, 1, 6);
|
||||
let data = DataSet::from_components(
|
||||
vec![
|
||||
Instrument {
|
||||
@@ -52,8 +55,8 @@ fn engine_settles_delisted_position_before_missing_market_snapshot_breaks_run()
|
||||
board: "SZ".to_string(),
|
||||
round_lot: 100,
|
||||
listed_at: Some(d(2020, 1, 1)),
|
||||
delisted_at: Some(date1),
|
||||
status: "delisted".to_string(),
|
||||
delisted_at: Some(delist_date),
|
||||
status: "active".to_string(),
|
||||
},
|
||||
Instrument {
|
||||
symbol: "000002.SZ".to_string(),
|
||||
@@ -80,7 +83,7 @@ fn engine_settles_delisted_position_before_missing_market_snapshot_breaks_run()
|
||||
ask1: 10.0,
|
||||
prev_close: 10.0,
|
||||
volume: 100_000,
|
||||
tick_volume: 100_000,
|
||||
minute_volume: 100_000,
|
||||
bid1_volume: 100_000,
|
||||
ask1_volume: 100_000,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
@@ -103,7 +106,7 @@ fn engine_settles_delisted_position_before_missing_market_snapshot_breaks_run()
|
||||
ask1: 5.01,
|
||||
prev_close: 5.0,
|
||||
volume: 100_000,
|
||||
tick_volume: 100_000,
|
||||
minute_volume: 100_000,
|
||||
bid1_volume: 100_000,
|
||||
ask1_volume: 100_000,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
@@ -113,9 +116,32 @@ fn engine_settles_delisted_position_before_missing_market_snapshot_breaks_run()
|
||||
price_tick: 0.01,
|
||||
},
|
||||
DailyMarketSnapshot {
|
||||
date: date2,
|
||||
date: delist_date,
|
||||
symbol: "000002.SZ".to_string(),
|
||||
timestamp: Some("2025-01-03 10:18:00".to_string()),
|
||||
day_open: 5.05,
|
||||
open: 5.05,
|
||||
high: 5.15,
|
||||
low: 5.0,
|
||||
close: 5.05,
|
||||
last_price: 5.05,
|
||||
bid1: 5.04,
|
||||
ask1: 5.06,
|
||||
prev_close: 5.0,
|
||||
volume: 110_000,
|
||||
minute_volume: 110_000,
|
||||
bid1_volume: 110_000,
|
||||
ask1_volume: 110_000,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
paused: false,
|
||||
upper_limit: 5.5,
|
||||
lower_limit: 4.5,
|
||||
price_tick: 0.01,
|
||||
},
|
||||
DailyMarketSnapshot {
|
||||
date: date2,
|
||||
symbol: "000002.SZ".to_string(),
|
||||
timestamp: Some("2025-01-06 10:18:00".to_string()),
|
||||
day_open: 5.1,
|
||||
open: 5.1,
|
||||
high: 5.2,
|
||||
@@ -126,7 +152,7 @@ fn engine_settles_delisted_position_before_missing_market_snapshot_breaks_run()
|
||||
ask1: 5.11,
|
||||
prev_close: 5.0,
|
||||
volume: 120_000,
|
||||
tick_volume: 120_000,
|
||||
minute_volume: 120_000,
|
||||
bid1_volume: 120_000,
|
||||
ask1_volume: 120_000,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
@@ -145,6 +171,7 @@ fn engine_settles_delisted_position_before_missing_market_snapshot_breaks_run()
|
||||
pe_ttm: 10.0,
|
||||
turnover_ratio: Some(1.0),
|
||||
effective_turnover_ratio: Some(1.0),
|
||||
adjustment_factor_backward1: None,
|
||||
extra_factors: BTreeMap::new(),
|
||||
},
|
||||
DailyFactorSnapshot {
|
||||
@@ -155,6 +182,18 @@ fn engine_settles_delisted_position_before_missing_market_snapshot_breaks_run()
|
||||
pe_ttm: 10.0,
|
||||
turnover_ratio: Some(1.0),
|
||||
effective_turnover_ratio: Some(1.0),
|
||||
adjustment_factor_backward1: None,
|
||||
extra_factors: BTreeMap::new(),
|
||||
},
|
||||
DailyFactorSnapshot {
|
||||
date: delist_date,
|
||||
symbol: "000002.SZ".to_string(),
|
||||
market_cap_bn: 30.5,
|
||||
free_float_cap_bn: 28.5,
|
||||
pe_ttm: 10.0,
|
||||
turnover_ratio: Some(1.0),
|
||||
effective_turnover_ratio: Some(1.0),
|
||||
adjustment_factor_backward1: None,
|
||||
extra_factors: BTreeMap::new(),
|
||||
},
|
||||
DailyFactorSnapshot {
|
||||
@@ -165,6 +204,7 @@ fn engine_settles_delisted_position_before_missing_market_snapshot_breaks_run()
|
||||
pe_ttm: 10.0,
|
||||
turnover_ratio: Some(1.0),
|
||||
effective_turnover_ratio: Some(1.0),
|
||||
adjustment_factor_backward1: None,
|
||||
extra_factors: BTreeMap::new(),
|
||||
},
|
||||
],
|
||||
@@ -173,34 +213,53 @@ fn engine_settles_delisted_position_before_missing_market_snapshot_breaks_run()
|
||||
date: date1,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
is_paused: false,
|
||||
allow_buy: true,
|
||||
allow_sell: true,
|
||||
is_kcb: false,
|
||||
is_one_yuan: false,
|
||||
risk_level_code: None,
|
||||
},
|
||||
CandidateEligibility {
|
||||
date: date1,
|
||||
symbol: "000002.SZ".to_string(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
is_paused: false,
|
||||
allow_buy: true,
|
||||
allow_sell: true,
|
||||
is_kcb: false,
|
||||
is_one_yuan: false,
|
||||
risk_level_code: None,
|
||||
},
|
||||
CandidateEligibility {
|
||||
date: delist_date,
|
||||
symbol: "000002.SZ".to_string(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
is_paused: false,
|
||||
allow_buy: true,
|
||||
allow_sell: true,
|
||||
is_kcb: false,
|
||||
is_one_yuan: false,
|
||||
risk_level_code: None,
|
||||
},
|
||||
CandidateEligibility {
|
||||
date: date2,
|
||||
symbol: "000002.SZ".to_string(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
is_paused: false,
|
||||
allow_buy: true,
|
||||
allow_sell: true,
|
||||
is_kcb: false,
|
||||
is_one_yuan: false,
|
||||
risk_level_code: None,
|
||||
},
|
||||
],
|
||||
vec![
|
||||
@@ -212,6 +271,14 @@ fn engine_settles_delisted_position_before_missing_market_snapshot_breaks_run()
|
||||
prev_close: 99.0,
|
||||
volume: 1_000_000,
|
||||
},
|
||||
BenchmarkSnapshot {
|
||||
date: delist_date,
|
||||
benchmark: "000300.SH".to_string(),
|
||||
open: 100.5,
|
||||
close: 100.5,
|
||||
prev_close: 100.0,
|
||||
volume: 1_050_000,
|
||||
},
|
||||
BenchmarkSnapshot {
|
||||
date: date2,
|
||||
benchmark: "000300.SH".to_string(),
|
||||
@@ -244,24 +311,33 @@ fn engine_settles_delisted_position_before_missing_market_snapshot_breaks_run()
|
||||
);
|
||||
|
||||
let result = engine.run().expect("backtest succeeds");
|
||||
assert_eq!(result.fills.len(), 2);
|
||||
assert_eq!(result.fills.len(), 1);
|
||||
assert!(
|
||||
result
|
||||
.fills
|
||||
.iter()
|
||||
.any(|fill| fill.reason.contains("delisted_cash_settlement")
|
||||
&& fill.symbol == "000001.SZ")
|
||||
);
|
||||
assert!(
|
||||
result
|
||||
.holdings_summary
|
||||
.iter()
|
||||
.all(|holding| holding.symbol != "000001.SZ")
|
||||
.all(|fill| !fill.reason.contains("delisted_cash_settlement"))
|
||||
);
|
||||
let unresolved = result
|
||||
.holdings_summary
|
||||
.iter()
|
||||
.find(|holding| holding.symbol == "000001.SZ")
|
||||
.expect("unresolved delisted holding remains auditable");
|
||||
assert_eq!(unresolved.quantity, 900);
|
||||
assert_eq!(unresolved.last_price, 0.0);
|
||||
assert_eq!(unresolved.market_value, 0.0);
|
||||
assert!(result.equity_curve.iter().any(|point| {
|
||||
point
|
||||
.notes
|
||||
.contains("unresolved_delisted_position symbol=000001.SZ")
|
||||
&& point.notes.contains("settlement_action=missing")
|
||||
&& point.notes.contains("valuation_policy=zero")
|
||||
&& point.notes.contains("no_order=true")
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn engine_applies_successor_conversion_before_delisted_cash_settlement() {
|
||||
fn engine_applies_successor_conversion_before_unresolved_delisting_audit() {
|
||||
let date1 = d(2025, 1, 2);
|
||||
let date2 = d(2025, 1, 3);
|
||||
let data = DataSet::from_components_with_actions(
|
||||
@@ -273,7 +349,7 @@ fn engine_applies_successor_conversion_before_delisted_cash_settlement() {
|
||||
round_lot: 100,
|
||||
listed_at: Some(d(2020, 1, 1)),
|
||||
delisted_at: Some(date2),
|
||||
status: "delisted".to_string(),
|
||||
status: "active".to_string(),
|
||||
},
|
||||
Instrument {
|
||||
symbol: "000002.SZ".to_string(),
|
||||
@@ -300,7 +376,7 @@ fn engine_applies_successor_conversion_before_delisted_cash_settlement() {
|
||||
ask1: 10.0,
|
||||
prev_close: 10.0,
|
||||
volume: 100_000,
|
||||
tick_volume: 100_000,
|
||||
minute_volume: 100_000,
|
||||
bid1_volume: 100_000,
|
||||
ask1_volume: 100_000,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
@@ -323,7 +399,7 @@ fn engine_applies_successor_conversion_before_delisted_cash_settlement() {
|
||||
ask1: 20.0,
|
||||
prev_close: 20.0,
|
||||
volume: 100_000,
|
||||
tick_volume: 100_000,
|
||||
minute_volume: 100_000,
|
||||
bid1_volume: 100_000,
|
||||
ask1_volume: 100_000,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
@@ -346,7 +422,7 @@ fn engine_applies_successor_conversion_before_delisted_cash_settlement() {
|
||||
ask1: 21.0,
|
||||
prev_close: 20.0,
|
||||
volume: 120_000,
|
||||
tick_volume: 120_000,
|
||||
minute_volume: 120_000,
|
||||
bid1_volume: 120_000,
|
||||
ask1_volume: 120_000,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
@@ -365,6 +441,7 @@ fn engine_applies_successor_conversion_before_delisted_cash_settlement() {
|
||||
pe_ttm: 10.0,
|
||||
turnover_ratio: Some(1.0),
|
||||
effective_turnover_ratio: Some(1.0),
|
||||
adjustment_factor_backward1: None,
|
||||
extra_factors: BTreeMap::new(),
|
||||
},
|
||||
DailyFactorSnapshot {
|
||||
@@ -375,6 +452,7 @@ fn engine_applies_successor_conversion_before_delisted_cash_settlement() {
|
||||
pe_ttm: 10.0,
|
||||
turnover_ratio: Some(1.0),
|
||||
effective_turnover_ratio: Some(1.0),
|
||||
adjustment_factor_backward1: None,
|
||||
extra_factors: BTreeMap::new(),
|
||||
},
|
||||
DailyFactorSnapshot {
|
||||
@@ -385,6 +463,7 @@ fn engine_applies_successor_conversion_before_delisted_cash_settlement() {
|
||||
pe_ttm: 10.0,
|
||||
turnover_ratio: Some(1.0),
|
||||
effective_turnover_ratio: Some(1.0),
|
||||
adjustment_factor_backward1: None,
|
||||
extra_factors: BTreeMap::new(),
|
||||
},
|
||||
],
|
||||
@@ -393,34 +472,40 @@ fn engine_applies_successor_conversion_before_delisted_cash_settlement() {
|
||||
date: date1,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
is_paused: false,
|
||||
allow_buy: true,
|
||||
allow_sell: true,
|
||||
is_kcb: false,
|
||||
is_one_yuan: false,
|
||||
risk_level_code: None,
|
||||
},
|
||||
CandidateEligibility {
|
||||
date: date1,
|
||||
symbol: "000002.SZ".to_string(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
is_paused: false,
|
||||
allow_buy: true,
|
||||
allow_sell: true,
|
||||
is_kcb: false,
|
||||
is_one_yuan: false,
|
||||
risk_level_code: None,
|
||||
},
|
||||
CandidateEligibility {
|
||||
date: date2,
|
||||
symbol: "000002.SZ".to_string(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
is_paused: false,
|
||||
allow_buy: true,
|
||||
allow_sell: true,
|
||||
is_kcb: false,
|
||||
is_one_yuan: false,
|
||||
risk_level_code: None,
|
||||
},
|
||||
],
|
||||
vec![
|
||||
@@ -492,7 +577,7 @@ fn engine_applies_successor_conversion_before_delisted_cash_settlement() {
|
||||
.iter()
|
||||
.find(|holding| holding.symbol == "000002.SZ")
|
||||
.expect("successor holding exists");
|
||||
assert_eq!(successor_holding.quantity, 500);
|
||||
assert_eq!(successor_holding.quantity, 450);
|
||||
assert!(
|
||||
result
|
||||
.holdings_summary
|
||||
@@ -503,6 +588,6 @@ fn engine_applies_successor_conversion_before_delisted_cash_settlement() {
|
||||
event
|
||||
.note
|
||||
.contains("successor_conversion 000001.SZ->000002.SZ")
|
||||
&& event.note.contains("cash=1000.00")
|
||||
&& event.note.contains("cash=900.00")
|
||||
}));
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -208,3 +208,134 @@ fn futures_expiration_settlement_closes_all_contract_directions() {
|
||||
);
|
||||
assert!((account.total_cash() - 1_003_000.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn futures_full_close_preserves_closed_position_daily_metrics() {
|
||||
let spec = FuturesContractSpec::new(10.0, 0.1, 0.1);
|
||||
let mut account = FuturesAccountState::new(100_000.0);
|
||||
account.open("IF2506.CCFX", FuturesDirection::Long, spec, 1, 100.0, 1.0);
|
||||
account.begin_trading_day();
|
||||
|
||||
let realized = account
|
||||
.close("IF2506.CCFX", FuturesDirection::Long, 1, 110.0, 2.0)
|
||||
.expect("close overnight position");
|
||||
|
||||
assert!(account.positions().is_empty());
|
||||
assert!((realized - 98.0).abs() < 1e-12);
|
||||
assert!((account.position_pnl() - 100.0).abs() < 1e-12);
|
||||
assert!(account.trading_pnl().abs() < 1e-12);
|
||||
assert!((account.transaction_cost() - 2.0).abs() < 1e-12);
|
||||
assert!((account.daily_pnl() - 98.0).abs() < 1e-12);
|
||||
assert!((account.total_cash() - 100_097.0).abs() < 1e-12);
|
||||
|
||||
account.begin_trading_day();
|
||||
assert!(account.daily_pnl().abs() < 1e-12);
|
||||
assert!(account.transaction_cost().abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn futures_intraday_roundtrip_preserves_closed_trading_pnl() {
|
||||
let spec = FuturesContractSpec::new(10.0, 0.1, 0.1);
|
||||
let mut account = FuturesAccountState::new(100_000.0);
|
||||
account.begin_trading_day();
|
||||
account.open("IF2506.CCFX", FuturesDirection::Long, spec, 1, 100.0, 1.0);
|
||||
account
|
||||
.close("IF2506.CCFX", FuturesDirection::Long, 1, 110.0, 2.0)
|
||||
.expect("close intraday position");
|
||||
|
||||
assert!(account.positions().is_empty());
|
||||
assert!((account.trading_pnl() - 100.0).abs() < 1e-12);
|
||||
assert!(account.position_pnl().abs() < 1e-12);
|
||||
assert!((account.transaction_cost() - 3.0).abs() < 1e-12);
|
||||
assert!((account.daily_pnl() - 97.0).abs() < 1e-12);
|
||||
assert!((account.total_cash() - 100_097.0).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn futures_partial_close_offsets_later_mark_with_trading_pnl() {
|
||||
let spec = FuturesContractSpec::new(10.0, 0.1, 0.1);
|
||||
let mut account = FuturesAccountState::new(100_000.0);
|
||||
account.open("IF2506.CCFX", FuturesDirection::Long, spec, 2, 100.0, 0.0);
|
||||
account.begin_trading_day();
|
||||
account
|
||||
.close("IF2506.CCFX", FuturesDirection::Long, 1, 110.0, 0.0)
|
||||
.expect("partially close overnight position");
|
||||
account.mark_price("IF2506.CCFX", FuturesDirection::Long, 120.0);
|
||||
|
||||
assert!((account.position_pnl() - 400.0).abs() < 1e-12);
|
||||
assert!((account.trading_pnl() + 100.0).abs() < 1e-12);
|
||||
assert!((account.daily_pnl() - 300.0).abs() < 1e-12);
|
||||
assert!((account.total_value() - 100_300.0).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn futures_settlement_keeps_same_day_pnl_visible_until_next_day() {
|
||||
let spec = FuturesContractSpec::new(10.0, 0.1, 0.1);
|
||||
let mut account = FuturesAccountState::new(100_000.0);
|
||||
account.open("IF2506.CCFX", FuturesDirection::Long, spec, 1, 100.0, 0.0);
|
||||
account.begin_trading_day();
|
||||
account.mark_price("IF2506.CCFX", FuturesDirection::Long, 110.0);
|
||||
|
||||
let settled = account.settle(&BTreeMap::from([("IF2506.CCFX".to_string(), 110.0)]));
|
||||
|
||||
assert!((settled - 100.0).abs() < 1e-12);
|
||||
assert!((account.daily_pnl() - 100.0).abs() < 1e-12);
|
||||
assert!((account.total_cash() - 100_100.0).abs() < 1e-12);
|
||||
assert!((account.total_value() - 100_100.0).abs() < 1e-12);
|
||||
|
||||
account.begin_trading_day();
|
||||
assert!(account.daily_pnl().abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn futures_cash_and_closed_cost_accumulate_micro_yuan_exactly() {
|
||||
let spec = FuturesContractSpec::new(1.0, 0.0, 0.0);
|
||||
let mut account = FuturesAccountState::new(1_000_000.0);
|
||||
account.begin_trading_day();
|
||||
for _ in 0..10_000 {
|
||||
account.open(
|
||||
"IF2506.CCFX",
|
||||
FuturesDirection::Long,
|
||||
spec,
|
||||
1,
|
||||
100.0,
|
||||
0.000001,
|
||||
);
|
||||
account
|
||||
.close("IF2506.CCFX", FuturesDirection::Long, 1, 100.0, 0.000001)
|
||||
.expect("close micro-cost position");
|
||||
}
|
||||
|
||||
assert!((account.total_cash() - 999_999.98).abs() < 1e-12);
|
||||
assert!((account.transaction_cost() - 0.02).abs() < 1e-12);
|
||||
assert!((account.daily_pnl() + 0.02).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn futures_margin_gate_and_fill_cash_use_exact_micro_yuan() {
|
||||
let date = d(2025, 1, 2);
|
||||
let spec = FuturesContractSpec::new(1.0, 1.0, 1.0);
|
||||
let intent = FuturesOrderIntent::open(
|
||||
"IF2506.CCFX",
|
||||
FuturesDirection::Long,
|
||||
spec,
|
||||
1,
|
||||
100.0,
|
||||
0.000001,
|
||||
"micro margin boundary",
|
||||
);
|
||||
|
||||
let mut insufficient = FuturesAccountState::new(100.0);
|
||||
let rejected = insufficient.execute_order(date, Some(1), intent.clone());
|
||||
assert_eq!(rejected.order_events[0].status, OrderStatus::Rejected);
|
||||
assert!((insufficient.total_cash() - 100.0).abs() < 1e-12);
|
||||
|
||||
let mut exact = FuturesAccountState::new(100.000001);
|
||||
let filled = exact.execute_order(date, Some(2), intent);
|
||||
assert_eq!(filled.order_events[0].status, OrderStatus::Filled);
|
||||
assert_eq!(filled.fill_events.len(), 1);
|
||||
assert!((filled.fill_events[0].gross_amount - 100.0).abs() < 1e-12);
|
||||
assert!((filled.fill_events[0].commission - 0.000001).abs() < 1e-12);
|
||||
assert!((filled.fill_events[0].net_cash_flow + 0.000001).abs() < 1e-12);
|
||||
assert!(exact.cash().abs() < 1e-12);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
use std::hint::black_box;
|
||||
use std::time::Instant;
|
||||
|
||||
use chrono::{Duration, NaiveDate, NaiveDateTime, NaiveTime};
|
||||
use fidc_core::{
|
||||
BenchmarkSnapshot, DailyMarketSnapshot, DataSet, Instrument, IntradayExecutionQuote,
|
||||
};
|
||||
|
||||
const SYMBOL: &str = "000001.SZ";
|
||||
|
||||
fn dataset(day_count: usize, bars_per_day: usize) -> (DataSet, Vec<NaiveDate>) {
|
||||
let start = NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid start date");
|
||||
let dates = (0..day_count)
|
||||
.map(|offset| start + Duration::days(offset as i64))
|
||||
.collect::<Vec<_>>();
|
||||
let markets = dates
|
||||
.iter()
|
||||
.map(|date| DailyMarketSnapshot {
|
||||
date: *date,
|
||||
symbol: SYMBOL.to_string(),
|
||||
timestamp: None,
|
||||
day_open: 10.0,
|
||||
open: 10.0,
|
||||
high: 10.5,
|
||||
low: 9.5,
|
||||
close: 10.0,
|
||||
last_price: 10.0,
|
||||
bid1: 9.99,
|
||||
ask1: 10.01,
|
||||
prev_close: 10.0,
|
||||
volume: 1_000_000,
|
||||
minute_volume: 1_000,
|
||||
bid1_volume: 10_000,
|
||||
ask1_volume: 10_000,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
paused: false,
|
||||
upper_limit: 11.0,
|
||||
lower_limit: 9.0,
|
||||
price_tick: 0.01,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let benchmarks = dates
|
||||
.iter()
|
||||
.map(|date| BenchmarkSnapshot {
|
||||
date: *date,
|
||||
benchmark: "000852.SH".to_string(),
|
||||
open: 1_000.0,
|
||||
close: 1_000.0,
|
||||
prev_close: 1_000.0,
|
||||
volume: 10_000_000,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut quotes = Vec::with_capacity(day_count * bars_per_day);
|
||||
for date in &dates {
|
||||
let session_start = date.and_hms_opt(9, 30, 0).expect("valid session start");
|
||||
for offset in 0..bars_per_day {
|
||||
let timestamp = session_start + Duration::minutes(offset as i64);
|
||||
quotes.push(IntradayExecutionQuote {
|
||||
date: *date,
|
||||
symbol: SYMBOL.to_string(),
|
||||
timestamp,
|
||||
last_price: 10.0 + offset as f64 / 10_000.0,
|
||||
bid1: 9.99,
|
||||
ask1: 10.01,
|
||||
bid1_volume: 10_000,
|
||||
ask1_volume: 10_000,
|
||||
volume_delta: 1_000,
|
||||
amount_delta: 10_000.0,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
});
|
||||
}
|
||||
}
|
||||
let data = DataSet::from_components_with_actions_and_quotes(
|
||||
vec![Instrument {
|
||||
symbol: SYMBOL.to_string(),
|
||||
name: "平安银行".to_string(),
|
||||
board: "SZ".to_string(),
|
||||
round_lot: 100,
|
||||
listed_at: Some(start - Duration::days(1_000)),
|
||||
delisted_at: None,
|
||||
status: "active".to_string(),
|
||||
}],
|
||||
markets,
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
benchmarks,
|
||||
Vec::new(),
|
||||
quotes,
|
||||
)
|
||||
.expect("build intraday history dataset");
|
||||
(data, dates)
|
||||
}
|
||||
|
||||
fn timestamp(date: NaiveDate, time: &str) -> NaiveDateTime {
|
||||
let time = NaiveTime::parse_from_str(time, "%H:%M:%S").expect("valid time");
|
||||
date.and_time(time)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intraday_history_is_bounded_by_visibility_and_preserves_order() {
|
||||
let (data, dates) = dataset(3, 4);
|
||||
let rows = data.history_intraday_quotes_at(
|
||||
dates[1],
|
||||
Some(timestamp(dates[1], "09:32:00")),
|
||||
SYMBOL,
|
||||
3,
|
||||
false,
|
||||
);
|
||||
assert_eq!(
|
||||
rows.iter().map(|row| row.timestamp).collect::<Vec<_>>(),
|
||||
vec![
|
||||
timestamp(dates[0], "09:33:00"),
|
||||
timestamp(dates[1], "09:30:00"),
|
||||
timestamp(dates[1], "09:31:00"),
|
||||
]
|
||||
);
|
||||
|
||||
let including_now = data.history_intraday_quotes_at(
|
||||
dates[1],
|
||||
Some(timestamp(dates[1], "09:32:00")),
|
||||
SYMBOL,
|
||||
3,
|
||||
true,
|
||||
);
|
||||
assert_eq!(
|
||||
including_now
|
||||
.iter()
|
||||
.map(|row| row.timestamp)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
timestamp(dates[1], "09:30:00"),
|
||||
timestamp(dates[1], "09:31:00"),
|
||||
timestamp(dates[1], "09:32:00"),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "manual release-mode intraday history benchmark"]
|
||||
fn benchmark_bounded_intraday_history() {
|
||||
let (data, dates) = dataset(250, 240);
|
||||
let active_datetime = timestamp(*dates.last().expect("last date"), "13:29:00");
|
||||
|
||||
for _ in 0..5 {
|
||||
black_box(data.history_intraday_quotes_at(
|
||||
active_datetime.date(),
|
||||
Some(active_datetime),
|
||||
SYMBOL,
|
||||
30,
|
||||
true,
|
||||
));
|
||||
}
|
||||
|
||||
let started = Instant::now();
|
||||
let mut checksum = 0_i64;
|
||||
for _ in 0..200 {
|
||||
let rows = data.history_intraday_quotes_at(
|
||||
active_datetime.date(),
|
||||
Some(active_datetime),
|
||||
SYMBOL,
|
||||
30,
|
||||
true,
|
||||
);
|
||||
checksum += rows
|
||||
.last()
|
||||
.expect("history row")
|
||||
.timestamp
|
||||
.and_utc()
|
||||
.timestamp();
|
||||
black_box(&rows);
|
||||
}
|
||||
let elapsed = started.elapsed();
|
||||
eprintln!(
|
||||
"intraday_history_benchmark iterations=200 rows_per_dataset=60000 elapsed_seconds={:.6} checksum={checksum}",
|
||||
elapsed.as_secs_f64(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "manual release-mode quote-stream benchmark"]
|
||||
fn benchmark_borrowed_execution_quote_stream() {
|
||||
let (data, dates) = dataset(250, 240);
|
||||
let date = *dates.last().expect("last date");
|
||||
let symbols = std::collections::BTreeSet::from([SYMBOL.to_string()]);
|
||||
|
||||
for _ in 0..5 {
|
||||
black_box(data.execution_quotes_on_date_for_symbols(date, Some(&symbols)));
|
||||
black_box(
|
||||
data.execution_quotes_iter_on_date_for_symbols(date, Some(&symbols))
|
||||
.count(),
|
||||
);
|
||||
}
|
||||
|
||||
let materialized_started = Instant::now();
|
||||
let mut materialized_checksum = 0_i64;
|
||||
for _ in 0..5_000 {
|
||||
let rows = data.execution_quotes_on_date_for_symbols(date, Some(&symbols));
|
||||
materialized_checksum += rows
|
||||
.iter()
|
||||
.map(|quote| quote.timestamp.and_utc().timestamp())
|
||||
.sum::<i64>();
|
||||
black_box(rows);
|
||||
}
|
||||
let materialized_seconds = materialized_started.elapsed().as_secs_f64();
|
||||
|
||||
let streamed_started = Instant::now();
|
||||
let mut streamed_checksum = 0_i64;
|
||||
for _ in 0..5_000 {
|
||||
let count = data
|
||||
.execution_quotes_iter_on_date_for_symbols(date, Some(&symbols))
|
||||
.map(|quote| quote.timestamp.and_utc().timestamp())
|
||||
.sum::<i64>();
|
||||
streamed_checksum += count;
|
||||
black_box(count);
|
||||
}
|
||||
let streamed_seconds = streamed_started.elapsed().as_secs_f64();
|
||||
eprintln!(
|
||||
"quote_stream_benchmark iterations=5000 rows_per_day=240 materialized_seconds={materialized_seconds:.6} streamed_seconds={streamed_seconds:.6} materialized_checksum={materialized_checksum} streamed_checksum={streamed_checksum}"
|
||||
);
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
use fidc_core::DataSet;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
fn temp_dir() -> PathBuf {
|
||||
let uniq = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("clock")
|
||||
.as_nanos();
|
||||
let dir = std::env::temp_dir().join(format!("fidc-bt-partitioned-{uniq}"));
|
||||
fs::create_dir_all(&dir).expect("mkdir temp");
|
||||
dir
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_load_partitioned_snapshot_dir() {
|
||||
let dir = temp_dir();
|
||||
fs::create_dir_all(dir.join("benchmark/2024/01")).unwrap();
|
||||
fs::create_dir_all(dir.join("market/2024/01")).unwrap();
|
||||
fs::create_dir_all(dir.join("factors/2024/01")).unwrap();
|
||||
fs::create_dir_all(dir.join("candidates/2024/01")).unwrap();
|
||||
fs::create_dir_all(dir.join("corporate_actions/2024/01")).unwrap();
|
||||
|
||||
fs::write(
|
||||
dir.join("instruments.csv"),
|
||||
"symbol,name,board,round_lot,listed_at,delisted_at,status\n000001.SZ,PingAn,SZ,100,2020-01-01,,active\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
dir.join("benchmark/2024/01/2024-01-02.csv"),
|
||||
"date,benchmark,open,close,prev_close,volume\n2024-01-02,CSI300.DEMO,2990,3000,2980,100000000\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
dir.join("market/2024/01/2024-01-02.csv"),
|
||||
"date,symbol,open,high,low,close,prev_close,volume,paused,upper_limit,lower_limit,day_open,last_price,bid1,ask1,price_tick\n2024-01-02,000001.SZ,10,10.5,9.9,10.2,10,100000,false,11,9,10.1,10.15,10.14,10.16,0.01\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
dir.join("factors/2024/01/2024-01-02.csv"),
|
||||
"date,symbol,market_cap_bn,free_float_cap_bn,pe_ttm,turnover_ratio,effective_turnover_ratio\n2024-01-02,000001.SZ,40,35,12,3.2,2.1\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
dir.join("candidates/2024/01/2024-01-02.csv"),
|
||||
"date,symbol,is_st,is_new_listing,is_paused,allow_buy,allow_sell,is_kcb,is_one_yuan\n2024-01-02,000001.SZ,false,false,false,true,true,false,false\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
dir.join("corporate_actions/2024/01/2024-01-02.csv"),
|
||||
"date,symbol,payable_date,share_cash,share_bonus,share_gift,issue_quantity,issue_price,reform,adjust_factor\n2024-01-02,000001.SZ,2024-01-05,0.5,0.1,0.0,0,0,false,1.05\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let data = DataSet::from_partitioned_dir(&dir).expect("partitioned dataset");
|
||||
assert_eq!(data.benchmark_code(), "CSI300.DEMO");
|
||||
assert!(
|
||||
data.market_snapshots_on(chrono::NaiveDate::from_ymd_opt(2024, 1, 2).unwrap())
|
||||
.len()
|
||||
== 1
|
||||
);
|
||||
let market_rows =
|
||||
data.market_snapshots_on(chrono::NaiveDate::from_ymd_opt(2024, 1, 2).unwrap());
|
||||
let snapshot = market_rows.first().expect("market snapshot");
|
||||
assert_eq!(snapshot.day_open, 10.1);
|
||||
assert_eq!(snapshot.last_price, 10.15);
|
||||
assert_eq!(snapshot.price_tick, 0.01);
|
||||
assert_eq!(
|
||||
data.instruments()
|
||||
.get("000001.SZ")
|
||||
.expect("instrument")
|
||||
.round_lot,
|
||||
100
|
||||
);
|
||||
assert_eq!(
|
||||
data.instruments()
|
||||
.get("000001.SZ")
|
||||
.expect("instrument")
|
||||
.listed_at,
|
||||
Some(chrono::NaiveDate::from_ymd_opt(2020, 1, 1).unwrap())
|
||||
);
|
||||
let actions = data.corporate_actions_on(chrono::NaiveDate::from_ymd_opt(2024, 1, 2).unwrap());
|
||||
assert_eq!(actions.len(), 1);
|
||||
assert_eq!(
|
||||
actions[0].payable_date,
|
||||
Some(chrono::NaiveDate::from_ymd_opt(2024, 1, 5).unwrap())
|
||||
);
|
||||
assert!((actions[0].share_cash - 0.5).abs() < 1e-9);
|
||||
assert!((actions[0].split_ratio() - 1.1).abs() < 1e-9);
|
||||
|
||||
let _ = fs::remove_dir_all(&dir);
|
||||
}
|
||||
@@ -1,15 +1,583 @@
|
||||
use chrono::NaiveDate;
|
||||
use fidc_core::{
|
||||
CnSmallCapRotationConfig, CnSmallCapRotationStrategy, DataSet, OmniMicroCapConfig,
|
||||
BenchmarkSnapshot, CandidateEligibility, CnSmallCapRotationConfig, CnSmallCapRotationStrategy,
|
||||
DailyFactorSnapshot, DailyMarketSnapshot, DataSet, Instrument, OmniMicroCapConfig,
|
||||
OmniMicroCapStrategy, PortfolioState, Strategy, StrategyContext,
|
||||
};
|
||||
use std::collections::BTreeSet;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn d(value: &str) -> NaiveDate {
|
||||
NaiveDate::parse_from_str(value, "%Y-%m-%d").unwrap()
|
||||
}
|
||||
|
||||
fn instrument(symbol: &str, name: &str) -> Instrument {
|
||||
Instrument {
|
||||
symbol: symbol.to_string(),
|
||||
name: name.to_string(),
|
||||
board: "Main".to_string(),
|
||||
round_lot: 100,
|
||||
listed_at: None,
|
||||
delisted_at: None,
|
||||
status: "active".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn market(
|
||||
date: &str,
|
||||
symbol: &str,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
prev_close: f64,
|
||||
volume: u64,
|
||||
paused: bool,
|
||||
) -> DailyMarketSnapshot {
|
||||
DailyMarketSnapshot {
|
||||
date: d(date),
|
||||
symbol: symbol.to_string(),
|
||||
timestamp: None,
|
||||
day_open: open,
|
||||
open,
|
||||
high,
|
||||
low,
|
||||
close,
|
||||
last_price: close,
|
||||
bid1: close,
|
||||
ask1: close,
|
||||
prev_close,
|
||||
volume,
|
||||
minute_volume: 0,
|
||||
bid1_volume: 0,
|
||||
ask1_volume: 0,
|
||||
trading_phase: None,
|
||||
paused,
|
||||
upper_limit: (prev_close * 1.10 * 100.0).round() / 100.0,
|
||||
lower_limit: (prev_close * 0.90 * 100.0).round() / 100.0,
|
||||
price_tick: 0.01,
|
||||
}
|
||||
}
|
||||
|
||||
fn factor(
|
||||
date: &str,
|
||||
symbol: &str,
|
||||
market_cap_bn: f64,
|
||||
free_float_cap_bn: f64,
|
||||
) -> DailyFactorSnapshot {
|
||||
DailyFactorSnapshot {
|
||||
date: d(date),
|
||||
symbol: symbol.to_string(),
|
||||
market_cap_bn,
|
||||
free_float_cap_bn,
|
||||
pe_ttm: 18.0,
|
||||
turnover_ratio: None,
|
||||
effective_turnover_ratio: None,
|
||||
adjustment_factor_backward1: None,
|
||||
extra_factors: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn candidate(
|
||||
date: &str,
|
||||
symbol: &str,
|
||||
is_new_listing: bool,
|
||||
is_paused: bool,
|
||||
allow_buy: bool,
|
||||
allow_sell: bool,
|
||||
) -> CandidateEligibility {
|
||||
CandidateEligibility {
|
||||
date: d(date),
|
||||
symbol: symbol.to_string(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing,
|
||||
is_paused,
|
||||
allow_buy,
|
||||
allow_sell,
|
||||
is_kcb: false,
|
||||
is_one_yuan: false,
|
||||
risk_level_code: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn benchmark(date: &str, open: f64, close: f64, prev_close: f64, volume: u64) -> BenchmarkSnapshot {
|
||||
BenchmarkSnapshot {
|
||||
date: d(date),
|
||||
benchmark: "CSI300.DEMO".to_string(),
|
||||
open,
|
||||
close,
|
||||
prev_close,
|
||||
volume,
|
||||
}
|
||||
}
|
||||
|
||||
fn strategy_test_dataset() -> DataSet {
|
||||
let dates = [
|
||||
"2024-01-02",
|
||||
"2024-01-03",
|
||||
"2024-01-04",
|
||||
"2024-01-05",
|
||||
"2024-01-08",
|
||||
"2024-01-09",
|
||||
"2024-01-10",
|
||||
"2024-01-11",
|
||||
"2024-01-12",
|
||||
];
|
||||
let instruments = vec![
|
||||
instrument("000001.SZ", "Alpha Components"),
|
||||
instrument("000002.SZ", "Beta Precision"),
|
||||
instrument("000003.SZ", "Charlie Materials"),
|
||||
instrument("600001.SH", "Delta Industrials"),
|
||||
];
|
||||
let market = vec![
|
||||
market(
|
||||
"2024-01-02",
|
||||
"000001.SZ",
|
||||
10.0,
|
||||
10.2,
|
||||
9.9,
|
||||
10.1,
|
||||
9.8,
|
||||
1_200_000,
|
||||
false,
|
||||
),
|
||||
market(
|
||||
"2024-01-02",
|
||||
"000002.SZ",
|
||||
11.0,
|
||||
11.3,
|
||||
10.9,
|
||||
11.2,
|
||||
10.8,
|
||||
1_100_000,
|
||||
false,
|
||||
),
|
||||
market(
|
||||
"2024-01-02",
|
||||
"000003.SZ",
|
||||
8.0,
|
||||
8.1,
|
||||
7.8,
|
||||
7.9,
|
||||
8.0,
|
||||
900_000,
|
||||
false,
|
||||
),
|
||||
market(
|
||||
"2024-01-02",
|
||||
"600001.SH",
|
||||
15.0,
|
||||
15.2,
|
||||
14.9,
|
||||
15.1,
|
||||
15.0,
|
||||
800_000,
|
||||
false,
|
||||
),
|
||||
market(
|
||||
"2024-01-03",
|
||||
"000001.SZ",
|
||||
10.2,
|
||||
10.5,
|
||||
10.1,
|
||||
10.4,
|
||||
10.1,
|
||||
1_250_000,
|
||||
false,
|
||||
),
|
||||
market(
|
||||
"2024-01-03",
|
||||
"000002.SZ",
|
||||
11.2,
|
||||
11.6,
|
||||
11.1,
|
||||
11.5,
|
||||
11.2,
|
||||
1_120_000,
|
||||
false,
|
||||
),
|
||||
market(
|
||||
"2024-01-03",
|
||||
"000003.SZ",
|
||||
7.8,
|
||||
7.9,
|
||||
7.3,
|
||||
7.4,
|
||||
7.9,
|
||||
930_000,
|
||||
false,
|
||||
),
|
||||
market(
|
||||
"2024-01-03",
|
||||
"600001.SH",
|
||||
15.1,
|
||||
15.3,
|
||||
15.0,
|
||||
15.2,
|
||||
15.1,
|
||||
820_000,
|
||||
false,
|
||||
),
|
||||
market(
|
||||
"2024-01-04",
|
||||
"000001.SZ",
|
||||
10.5,
|
||||
10.8,
|
||||
10.4,
|
||||
10.7,
|
||||
10.4,
|
||||
1_280_000,
|
||||
false,
|
||||
),
|
||||
market(
|
||||
"2024-01-04",
|
||||
"000002.SZ",
|
||||
11.4,
|
||||
11.9,
|
||||
11.3,
|
||||
11.8,
|
||||
11.5,
|
||||
1_150_000,
|
||||
false,
|
||||
),
|
||||
market(
|
||||
"2024-01-04",
|
||||
"000003.SZ",
|
||||
7.3,
|
||||
7.4,
|
||||
7.0,
|
||||
7.1,
|
||||
7.4,
|
||||
940_000,
|
||||
false,
|
||||
),
|
||||
market(
|
||||
"2024-01-04",
|
||||
"600001.SH",
|
||||
15.2,
|
||||
15.5,
|
||||
15.1,
|
||||
15.4,
|
||||
15.2,
|
||||
830_000,
|
||||
false,
|
||||
),
|
||||
market(
|
||||
"2024-01-05",
|
||||
"000001.SZ",
|
||||
10.8,
|
||||
11.1,
|
||||
10.7,
|
||||
11.0,
|
||||
10.7,
|
||||
1_300_000,
|
||||
false,
|
||||
),
|
||||
market(
|
||||
"2024-01-05",
|
||||
"000002.SZ",
|
||||
11.9,
|
||||
12.1,
|
||||
11.8,
|
||||
12.0,
|
||||
11.8,
|
||||
1_180_000,
|
||||
false,
|
||||
),
|
||||
market(
|
||||
"2024-01-05",
|
||||
"000003.SZ",
|
||||
7.0,
|
||||
7.1,
|
||||
6.8,
|
||||
6.9,
|
||||
7.1,
|
||||
950_000,
|
||||
false,
|
||||
),
|
||||
market(
|
||||
"2024-01-05",
|
||||
"600001.SH",
|
||||
15.4,
|
||||
15.6,
|
||||
15.3,
|
||||
15.5,
|
||||
15.4,
|
||||
840_000,
|
||||
false,
|
||||
),
|
||||
market(
|
||||
"2024-01-08",
|
||||
"000001.SZ",
|
||||
11.1,
|
||||
11.6,
|
||||
11.0,
|
||||
11.5,
|
||||
11.0,
|
||||
1_400_000,
|
||||
false,
|
||||
),
|
||||
market(
|
||||
"2024-01-08",
|
||||
"000002.SZ",
|
||||
12.1,
|
||||
12.5,
|
||||
12.0,
|
||||
12.4,
|
||||
12.0,
|
||||
1_200_000,
|
||||
false,
|
||||
),
|
||||
market(
|
||||
"2024-01-08",
|
||||
"000003.SZ",
|
||||
7.0,
|
||||
7.3,
|
||||
6.9,
|
||||
7.2,
|
||||
6.9,
|
||||
980_000,
|
||||
false,
|
||||
),
|
||||
market(
|
||||
"2024-01-08",
|
||||
"600001.SH",
|
||||
15.5,
|
||||
15.7,
|
||||
15.4,
|
||||
15.6,
|
||||
15.5,
|
||||
850_000,
|
||||
false,
|
||||
),
|
||||
market(
|
||||
"2024-01-09",
|
||||
"000001.SZ",
|
||||
11.6,
|
||||
12.4,
|
||||
11.5,
|
||||
12.3,
|
||||
11.5,
|
||||
1_500_000,
|
||||
false,
|
||||
),
|
||||
market(
|
||||
"2024-01-09",
|
||||
"000002.SZ",
|
||||
12.5,
|
||||
12.9,
|
||||
12.4,
|
||||
12.8,
|
||||
12.4,
|
||||
1_250_000,
|
||||
false,
|
||||
),
|
||||
market(
|
||||
"2024-01-09",
|
||||
"000003.SZ",
|
||||
7.2,
|
||||
7.5,
|
||||
7.1,
|
||||
7.4,
|
||||
7.2,
|
||||
990_000,
|
||||
false,
|
||||
),
|
||||
market(
|
||||
"2024-01-09",
|
||||
"600001.SH",
|
||||
15.6,
|
||||
15.7,
|
||||
15.4,
|
||||
15.5,
|
||||
15.6,
|
||||
860_000,
|
||||
false,
|
||||
),
|
||||
market(
|
||||
"2024-01-10",
|
||||
"000001.SZ",
|
||||
12.2,
|
||||
12.3,
|
||||
11.9,
|
||||
12.0,
|
||||
12.3,
|
||||
1_450_000,
|
||||
false,
|
||||
),
|
||||
market(
|
||||
"2024-01-10",
|
||||
"000002.SZ",
|
||||
12.7,
|
||||
12.8,
|
||||
12.5,
|
||||
12.6,
|
||||
12.8,
|
||||
1_220_000,
|
||||
false,
|
||||
),
|
||||
market(
|
||||
"2024-01-10",
|
||||
"000003.SZ",
|
||||
7.5,
|
||||
7.6,
|
||||
7.4,
|
||||
7.5,
|
||||
7.4,
|
||||
1_000_000,
|
||||
false,
|
||||
),
|
||||
market(
|
||||
"2024-01-10",
|
||||
"600001.SH",
|
||||
15.4,
|
||||
15.5,
|
||||
15.1,
|
||||
15.2,
|
||||
15.5,
|
||||
870_000,
|
||||
false,
|
||||
),
|
||||
market(
|
||||
"2024-01-11",
|
||||
"000001.SZ",
|
||||
12.0,
|
||||
12.1,
|
||||
11.5,
|
||||
11.6,
|
||||
12.0,
|
||||
1_420_000,
|
||||
false,
|
||||
),
|
||||
market(
|
||||
"2024-01-11",
|
||||
"000002.SZ",
|
||||
12.5,
|
||||
12.6,
|
||||
12.1,
|
||||
12.2,
|
||||
12.6,
|
||||
1_210_000,
|
||||
false,
|
||||
),
|
||||
market(
|
||||
"2024-01-11",
|
||||
"000003.SZ",
|
||||
7.4,
|
||||
7.5,
|
||||
7.2,
|
||||
7.3,
|
||||
7.5,
|
||||
980_000,
|
||||
false,
|
||||
),
|
||||
market(
|
||||
"2024-01-11",
|
||||
"600001.SH",
|
||||
15.2,
|
||||
15.2,
|
||||
15.2,
|
||||
15.2,
|
||||
15.2,
|
||||
0,
|
||||
true,
|
||||
),
|
||||
market(
|
||||
"2024-01-12",
|
||||
"000001.SZ",
|
||||
11.5,
|
||||
11.6,
|
||||
11.1,
|
||||
11.2,
|
||||
11.6,
|
||||
1_380_000,
|
||||
false,
|
||||
),
|
||||
market(
|
||||
"2024-01-12",
|
||||
"000002.SZ",
|
||||
12.1,
|
||||
12.2,
|
||||
11.8,
|
||||
11.9,
|
||||
12.2,
|
||||
1_190_000,
|
||||
false,
|
||||
),
|
||||
market(
|
||||
"2024-01-12",
|
||||
"000003.SZ",
|
||||
7.2,
|
||||
7.2,
|
||||
6.9,
|
||||
7.0,
|
||||
7.3,
|
||||
960_000,
|
||||
false,
|
||||
),
|
||||
market(
|
||||
"2024-01-12",
|
||||
"600001.SH",
|
||||
14.8,
|
||||
15.0,
|
||||
14.7,
|
||||
14.9,
|
||||
15.2,
|
||||
850_000,
|
||||
false,
|
||||
),
|
||||
];
|
||||
let factors = dates
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(idx, date)| {
|
||||
let i = idx as f64;
|
||||
[
|
||||
factor(date, "000001.SZ", 38.0 + i, 24.0 + i * 0.5),
|
||||
factor(date, "000002.SZ", 45.0 + i, 30.0 + i * 0.5),
|
||||
factor(date, "000003.SZ", 65.0 - i, 40.0 - i * 0.5),
|
||||
factor(date, "600001.SH", 85.0 + i, 55.0 + i * 0.5),
|
||||
]
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let candidates = dates
|
||||
.iter()
|
||||
.flat_map(|date| {
|
||||
let first_two = *date == "2024-01-02" || *date == "2024-01-03";
|
||||
let paused_600001 = *date == "2024-01-11";
|
||||
[
|
||||
candidate(date, "000001.SZ", first_two, false, !first_two, true),
|
||||
candidate(date, "000002.SZ", false, false, true, true),
|
||||
candidate(date, "000003.SZ", false, false, true, true),
|
||||
candidate(
|
||||
date,
|
||||
"600001.SH",
|
||||
false,
|
||||
paused_600001,
|
||||
!paused_600001,
|
||||
!paused_600001,
|
||||
),
|
||||
]
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let benchmarks = vec![
|
||||
benchmark("2024-01-02", 2990.0, 3000.0, 2980.0, 100_000_000),
|
||||
benchmark("2024-01-03", 3005.0, 3020.0, 3000.0, 102_000_000),
|
||||
benchmark("2024-01-04", 3025.0, 3050.0, 3020.0, 105_000_000),
|
||||
benchmark("2024-01-05", 3055.0, 3080.0, 3050.0, 108_000_000),
|
||||
benchmark("2024-01-08", 3085.0, 3110.0, 3080.0, 109_000_000),
|
||||
benchmark("2024-01-09", 3100.0, 3090.0, 3110.0, 107_000_000),
|
||||
benchmark("2024-01-10", 3080.0, 3040.0, 3090.0, 111_000_000),
|
||||
benchmark("2024-01-11", 3030.0, 2990.0, 3040.0, 115_000_000),
|
||||
benchmark("2024-01-12", 2980.0, 2950.0, 2990.0, 118_000_000),
|
||||
];
|
||||
DataSet::from_components(instruments, market, factors, candidates, benchmarks)
|
||||
.expect("strategy test dataset")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strategy_emits_target_weights_and_diagnostics() {
|
||||
let data_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../data/demo");
|
||||
let data = DataSet::from_csv_dir(&data_dir).expect("demo data");
|
||||
let data = strategy_test_dataset();
|
||||
let decision_date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
|
||||
let execution_date = NaiveDate::from_ymd_opt(2024, 1, 11).unwrap();
|
||||
let portfolio = PortfolioState::new(1_000_000.0);
|
||||
@@ -53,8 +621,7 @@ fn strategy_emits_target_weights_and_diagnostics() {
|
||||
|
||||
#[test]
|
||||
fn omni_strategy_emits_same_day_decision() {
|
||||
let data_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../data/demo");
|
||||
let data = DataSet::from_csv_dir(&data_dir).expect("demo data");
|
||||
let data = strategy_test_dataset();
|
||||
let execution_date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
|
||||
let portfolio = PortfolioState::new(1_000_000.0);
|
||||
let mut cfg = OmniMicroCapConfig::omni_microcap();
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
let refresh_rate = 15;
|
||||
let stocknum = 40;
|
||||
let close_rate = 1.07;
|
||||
let loss_rate = 0.93;
|
||||
let rsi_rate = 1.0001;
|
||||
let trade_rate = 0.5;
|
||||
let xs = 4 / 500;
|
||||
let base_index_level = 2000;
|
||||
let base_cap_floor = 3;
|
||||
let base_cap_ceiling = 28;
|
||||
|
||||
fn band_start(current_price, base_index_level, xs, base_cap_floor) {
|
||||
if current_price == base_index_level {
|
||||
base_cap_floor
|
||||
} else if current_price > 0 {
|
||||
round((current_price - base_index_level) * xs + base_cap_floor)
|
||||
} else {
|
||||
base_cap_floor
|
||||
}
|
||||
}
|
||||
|
||||
fn band_end(current_price, base_index_level, xs, base_cap_ceiling) {
|
||||
if current_price == base_index_level {
|
||||
base_cap_ceiling
|
||||
} else if current_price > 0 {
|
||||
round((current_price - base_index_level) * xs + base_cap_ceiling)
|
||||
} else {
|
||||
base_cap_ceiling
|
||||
}
|
||||
}
|
||||
|
||||
strategy("microcap_volume_trend_000852") {
|
||||
market("CN_A")
|
||||
benchmark("000852.SH")
|
||||
signal("000852.SH")
|
||||
|
||||
rebalance.every_days(refresh_rate).at("10:18")
|
||||
|
||||
universe.exclude("paused", "st", "kcb", "one_yuan", "new_listing")
|
||||
|
||||
selection.limit(stocknum)
|
||||
selection.market_cap_band(
|
||||
field="market_cap",
|
||||
lower=band_start(signal_close, base_index_level, xs, base_cap_floor),
|
||||
upper=band_end(signal_close, base_index_level, xs, base_cap_ceiling)
|
||||
)
|
||||
|
||||
risk.index_exposure(
|
||||
signal_ma5 > signal_ma10 * rsi_rate ? 1.0 : trade_rate
|
||||
)
|
||||
|
||||
filter.stock_expr(
|
||||
stock_ma5 > stock_ma10 * rsi_rate &&
|
||||
stock_ma10 > stock_ma30 * rsi_rate &&
|
||||
rolling_mean("volume", 5) < rolling_mean("volume", 60)
|
||||
)
|
||||
|
||||
risk.take_profit(close_rate)
|
||||
risk.stop_loss(loss_rate)
|
||||
allocation.buy_scale(touched_upper_limit ? 1.0 : trade_rate)
|
||||
|
||||
ordering.rank_by("market_cap", "asc")
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
{
|
||||
"strategyId": "microcap_volume_trend_000852",
|
||||
"version": "2",
|
||||
"parser": "omniquant-engine-script-v2",
|
||||
"market": "CN_A",
|
||||
"signalSymbol": "000852.SH",
|
||||
"benchmark": {
|
||||
"instrumentId": "000852.SH",
|
||||
"fallbackInstrumentId": "000852.SH"
|
||||
},
|
||||
"engineConfig": {
|
||||
"market": "CN_A",
|
||||
"signalSymbol": "000852.SH",
|
||||
"benchmarkSymbol": "000852.SH",
|
||||
"refreshRate": 15,
|
||||
"rankLimit": 40
|
||||
},
|
||||
"runtimeExpressions": {
|
||||
"prelude": "let refresh_rate = 15;\nlet stocknum = 40;\nlet close_rate = 1.07;\nlet loss_rate = 0.93;\nlet rsi_rate = 1.0001;\nlet trade_rate = 0.5;\nlet xs = 4 / 500;\nlet base_index_level = 2000;\nlet base_cap_floor = 3;\nlet base_cap_ceiling = 28;\nfn band_start(current_price, base_index_level, xs, base_cap_floor) {\n if current_price == base_index_level {\n base_cap_floor\n } else if current_price > 0 {\n round((current_price - base_index_level) * xs + base_cap_floor)\n } else {\n base_cap_floor\n }\n}\nfn band_end(current_price, base_index_level, xs, base_cap_ceiling) {\n if current_price == base_index_level {\n base_cap_ceiling\n } else if current_price > 0 {\n round((current_price - base_index_level) * xs + base_cap_ceiling)\n } else {\n base_cap_ceiling\n }\n}",
|
||||
"selection": {
|
||||
"limitExpr": "stocknum",
|
||||
"marketCapField": "market_cap",
|
||||
"marketCapLowerExpr": "band_start(signal_close, base_index_level, xs, base_cap_floor)",
|
||||
"marketCapUpperExpr": "band_end(signal_close, base_index_level, xs, base_cap_ceiling)",
|
||||
"stockFilterExpr": "stock_ma5 > stock_ma10 * rsi_rate && stock_ma10 > stock_ma30 * rsi_rate && rolling_mean(\"volume\", 5) < rolling_mean(\"volume\", 60)"
|
||||
},
|
||||
"risk": {
|
||||
"exposureExpr": "signal_ma5 > signal_ma10 * rsi_rate ? 1.0 : trade_rate",
|
||||
"stopLossExpr": "loss_rate",
|
||||
"takeProfitExpr": "close_rate"
|
||||
},
|
||||
"allocation": {
|
||||
"buyScaleExpr": "touched_upper_limit ? 1.0 : trade_rate"
|
||||
},
|
||||
"ordering": {
|
||||
"rankBy": "market_cap",
|
||||
"rankExpr": "",
|
||||
"rankOrder": "asc"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
let refresh_rate = 15;
|
||||
let stocknum = 40;
|
||||
let xs = 0.008;
|
||||
let base_index_level = 2000;
|
||||
let lower_offset = 3;
|
||||
let upper_offset = 28;
|
||||
|
||||
fn cap_floor(current_price, base_index_level, xs, lower_offset) {
|
||||
round((current_price - base_index_level) * xs + lower_offset)
|
||||
}
|
||||
|
||||
fn cap_ceiling(current_price, base_index_level, xs, upper_offset) {
|
||||
round((current_price - base_index_level) * xs + upper_offset)
|
||||
}
|
||||
|
||||
strategy("ai_generated_000001_open_cap_band") {
|
||||
market("CN_A")
|
||||
benchmark("000852.SH")
|
||||
signal("000001.SH")
|
||||
|
||||
rebalance.every_days(refresh_rate).at("10:18")
|
||||
|
||||
universe.exclude("paused", "st", "kcb", "one_yuan", "new_listing")
|
||||
|
||||
selection.limit(stocknum)
|
||||
selection.market_cap_band(
|
||||
field="market_cap",
|
||||
lower=cap_floor(signal_open, base_index_level, xs, lower_offset),
|
||||
upper=cap_ceiling(signal_open, base_index_level, xs, upper_offset)
|
||||
)
|
||||
|
||||
filter.stock_expr(
|
||||
stock_ma5 > stock_ma10 &&
|
||||
stock_ma10 > stock_ma30 &&
|
||||
rolling_mean("volume", 5) < rolling_mean("volume", 60) &&
|
||||
!ends_with(symbol, ".BJ") &&
|
||||
!at_upper_limit &&
|
||||
!at_lower_limit
|
||||
)
|
||||
|
||||
ordering.rank_by("market_cap", "asc")
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"strategyId": "ai_generated_000001_open_cap_band",
|
||||
"version": "2",
|
||||
"parser": "omniquant-engine-script-v2",
|
||||
"market": "CN_A",
|
||||
"signalSymbol": "000001.SH",
|
||||
"benchmark": {
|
||||
"instrumentId": "000852.SH",
|
||||
"fallbackInstrumentId": "000852.SH"
|
||||
},
|
||||
"engineConfig": {
|
||||
"market": "CN_A",
|
||||
"signalSymbol": "000001.SH",
|
||||
"benchmarkSymbol": "000852.SH",
|
||||
"refreshRate": 15,
|
||||
"rankLimit": 40
|
||||
},
|
||||
"runtimeExpressions": {
|
||||
"prelude": "let refresh_rate = 15;\nlet stocknum = 40;\nlet xs = 0.008;\nlet base_index_level = 2000;\nlet lower_offset = 3;\nlet upper_offset = 28;\n\nfn cap_floor(current_price, base_index_level, xs, lower_offset) {\nround((current_price - base_index_level) * xs + lower_offset)\n}\n\nfn cap_ceiling(current_price, base_index_level, xs, upper_offset) {\nround((current_price - base_index_level) * xs + upper_offset)\n}",
|
||||
"selection": {
|
||||
"limitExpr": "stocknum",
|
||||
"marketCapField": "market_cap",
|
||||
"marketCapLowerExpr": "cap_floor(signal_open, base_index_level, xs, lower_offset)",
|
||||
"marketCapUpperExpr": "cap_ceiling(signal_open, base_index_level, xs, upper_offset)",
|
||||
"stockFilterExpr": "stock_ma5 > stock_ma10 && stock_ma10 > stock_ma30 && rolling_mean(\"volume\", 5) < rolling_mean(\"volume\", 60) && !ends_with(symbol, \".BJ\") && !at_upper_limit && !at_lower_limit"
|
||||
},
|
||||
"ordering": {
|
||||
"rankBy": "market_cap",
|
||||
"rankExpr": "",
|
||||
"rankOrder": "asc"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
date,benchmark,open,close,prev_close,volume
|
||||
2024-01-02,CSI300.DEMO,2990,3000,2980,100000000
|
||||
2024-01-03,CSI300.DEMO,3005,3020,3000,102000000
|
||||
2024-01-04,CSI300.DEMO,3025,3050,3020,105000000
|
||||
2024-01-05,CSI300.DEMO,3055,3080,3050,108000000
|
||||
2024-01-08,CSI300.DEMO,3085,3110,3080,109000000
|
||||
2024-01-09,CSI300.DEMO,3100,3090,3110,107000000
|
||||
2024-01-10,CSI300.DEMO,3080,3040,3090,111000000
|
||||
2024-01-11,CSI300.DEMO,3030,2990,3040,115000000
|
||||
2024-01-12,CSI300.DEMO,2980,2950,2990,118000000
|
||||
|
@@ -1,37 +0,0 @@
|
||||
date,symbol,is_st,is_new_listing,is_paused,allow_buy,allow_sell,is_kcb,is_one_yuan
|
||||
2024-01-02,000001.SZ,false,true,false,false,true,false,false
|
||||
2024-01-02,000002.SZ,false,false,false,true,true,false,false
|
||||
2024-01-02,000003.SZ,false,false,false,true,true,false,false
|
||||
2024-01-02,600001.SH,false,false,false,true,true,false,false
|
||||
2024-01-03,000001.SZ,false,true,false,false,true,false,false
|
||||
2024-01-03,000002.SZ,false,false,false,true,true,false,false
|
||||
2024-01-03,000003.SZ,false,false,false,true,true,false,false
|
||||
2024-01-03,600001.SH,false,false,false,true,true,false,false
|
||||
2024-01-04,000001.SZ,false,false,false,true,true,false,false
|
||||
2024-01-04,000002.SZ,false,false,false,true,true,false,false
|
||||
2024-01-04,000003.SZ,false,false,false,true,true,false,false
|
||||
2024-01-04,600001.SH,false,false,false,true,true,false,false
|
||||
2024-01-05,000001.SZ,false,false,false,true,true,false,false
|
||||
2024-01-05,000002.SZ,false,false,false,true,true,false,false
|
||||
2024-01-05,000003.SZ,false,false,false,true,true,false,false
|
||||
2024-01-05,600001.SH,false,false,false,true,true,false,false
|
||||
2024-01-08,000001.SZ,false,false,false,true,true,false,false
|
||||
2024-01-08,000002.SZ,false,false,false,true,true,false,false
|
||||
2024-01-08,000003.SZ,false,false,false,true,true,false,false
|
||||
2024-01-08,600001.SH,false,false,false,true,true,false,false
|
||||
2024-01-09,000001.SZ,false,false,false,true,true,false,false
|
||||
2024-01-09,000002.SZ,false,false,false,true,true,false,false
|
||||
2024-01-09,000003.SZ,false,false,false,true,true,false,false
|
||||
2024-01-09,600001.SH,false,false,false,true,true,false,false
|
||||
2024-01-10,000001.SZ,false,false,false,true,true,false,false
|
||||
2024-01-10,000002.SZ,false,false,false,true,true,false,false
|
||||
2024-01-10,000003.SZ,false,false,false,true,true,false,false
|
||||
2024-01-10,600001.SH,false,false,false,true,true,false,false
|
||||
2024-01-11,000001.SZ,false,false,false,true,true,false,false
|
||||
2024-01-11,000002.SZ,false,false,false,true,true,false,false
|
||||
2024-01-11,000003.SZ,false,false,false,true,true,false,false
|
||||
2024-01-11,600001.SH,false,false,true,false,false,false,false
|
||||
2024-01-12,000001.SZ,false,false,false,true,true,false,false
|
||||
2024-01-12,000002.SZ,false,false,false,true,true,false,false
|
||||
2024-01-12,000003.SZ,false,false,false,true,true,false,false
|
||||
2024-01-12,600001.SH,false,false,false,true,true,false,false
|
||||
|
@@ -1,37 +0,0 @@
|
||||
date,symbol,market_cap_bn,free_float_cap_bn,pe_ttm
|
||||
2024-01-02,000001.SZ,38,24,18
|
||||
2024-01-02,000002.SZ,45,30,20
|
||||
2024-01-02,000003.SZ,65,40,15
|
||||
2024-01-02,600001.SH,85,55,13
|
||||
2024-01-03,000001.SZ,39,24.5,18
|
||||
2024-01-03,000002.SZ,46,30.5,20
|
||||
2024-01-03,000003.SZ,64,39.5,15
|
||||
2024-01-03,600001.SH,85,55,13
|
||||
2024-01-04,000001.SZ,40,25,18
|
||||
2024-01-04,000002.SZ,47,31,20
|
||||
2024-01-04,000003.SZ,63,39,15
|
||||
2024-01-04,600001.SH,86,55.5,13
|
||||
2024-01-05,000001.SZ,41,25.5,18
|
||||
2024-01-05,000002.SZ,48,32,20
|
||||
2024-01-05,000003.SZ,62,38.5,15
|
||||
2024-01-05,600001.SH,86,56,13
|
||||
2024-01-08,000001.SZ,42,26,18
|
||||
2024-01-08,000002.SZ,50,33,21
|
||||
2024-01-08,000003.SZ,61,38,15
|
||||
2024-01-08,600001.SH,87,56.5,13
|
||||
2024-01-09,000001.SZ,44,27,19
|
||||
2024-01-09,000002.SZ,52,34,21
|
||||
2024-01-09,000003.SZ,60,37.5,15
|
||||
2024-01-09,600001.SH,88,57,13
|
||||
2024-01-10,000001.SZ,43,26.5,19
|
||||
2024-01-10,000002.SZ,53,34.5,21
|
||||
2024-01-10,000003.SZ,59,37,15
|
||||
2024-01-10,600001.SH,89,57.5,13
|
||||
2024-01-11,000001.SZ,42,26,18
|
||||
2024-01-11,000002.SZ,52,34,21
|
||||
2024-01-11,000003.SZ,58,36.5,15
|
||||
2024-01-11,600001.SH,90,58,13
|
||||
2024-01-12,000001.SZ,40,25,18
|
||||
2024-01-12,000002.SZ,50,33,20
|
||||
2024-01-12,000003.SZ,57,36,15
|
||||
2024-01-12,600001.SH,92,59,13
|
||||
|
@@ -1,5 +0,0 @@
|
||||
symbol,name,board
|
||||
000001.SZ,Alpha Components,Main
|
||||
000002.SZ,Beta Precision,Main
|
||||
000003.SZ,Charlie Materials,Main
|
||||
600001.SH,Delta Industrials,Main
|
||||
|
@@ -1,37 +0,0 @@
|
||||
date,symbol,open,high,low,close,prev_close,volume,paused
|
||||
2024-01-02,000001.SZ,10.0,10.2,9.9,10.1,9.8,1200000,false
|
||||
2024-01-02,000002.SZ,11.0,11.3,10.9,11.2,10.8,1100000,false
|
||||
2024-01-02,000003.SZ,8.0,8.1,7.8,7.9,8.0,900000,false
|
||||
2024-01-02,600001.SH,15.0,15.2,14.9,15.1,15.0,800000,false
|
||||
2024-01-03,000001.SZ,10.2,10.5,10.1,10.4,10.1,1250000,false
|
||||
2024-01-03,000002.SZ,11.2,11.6,11.1,11.5,11.2,1120000,false
|
||||
2024-01-03,000003.SZ,7.8,7.9,7.3,7.4,7.9,930000,false
|
||||
2024-01-03,600001.SH,15.1,15.3,15.0,15.2,15.1,820000,false
|
||||
2024-01-04,000001.SZ,10.5,10.8,10.4,10.7,10.4,1280000,false
|
||||
2024-01-04,000002.SZ,11.4,11.9,11.3,11.8,11.5,1150000,false
|
||||
2024-01-04,000003.SZ,7.3,7.4,7.0,7.1,7.4,940000,false
|
||||
2024-01-04,600001.SH,15.2,15.5,15.1,15.4,15.2,830000,false
|
||||
2024-01-05,000001.SZ,10.8,11.1,10.7,11.0,10.7,1300000,false
|
||||
2024-01-05,000002.SZ,11.9,12.1,11.8,12.0,11.8,1180000,false
|
||||
2024-01-05,000003.SZ,7.0,7.1,6.8,6.9,7.1,950000,false
|
||||
2024-01-05,600001.SH,15.4,15.6,15.3,15.5,15.4,840000,false
|
||||
2024-01-08,000001.SZ,11.1,11.6,11.0,11.5,11.0,1400000,false
|
||||
2024-01-08,000002.SZ,12.1,12.5,12.0,12.4,12.0,1200000,false
|
||||
2024-01-08,000003.SZ,7.0,7.3,6.9,7.2,6.9,980000,false
|
||||
2024-01-08,600001.SH,15.5,15.7,15.4,15.6,15.5,850000,false
|
||||
2024-01-09,000001.SZ,11.6,12.4,11.5,12.3,11.5,1500000,false
|
||||
2024-01-09,000002.SZ,12.5,12.9,12.4,12.8,12.4,1250000,false
|
||||
2024-01-09,000003.SZ,7.2,7.5,7.1,7.4,7.2,990000,false
|
||||
2024-01-09,600001.SH,15.6,15.7,15.4,15.5,15.6,860000,false
|
||||
2024-01-10,000001.SZ,12.2,12.3,11.9,12.0,12.3,1450000,false
|
||||
2024-01-10,000002.SZ,12.7,12.8,12.5,12.6,12.8,1220000,false
|
||||
2024-01-10,000003.SZ,7.5,7.6,7.4,7.5,7.4,1000000,false
|
||||
2024-01-10,600001.SH,15.4,15.5,15.1,15.2,15.5,870000,false
|
||||
2024-01-11,000001.SZ,12.0,12.1,11.5,11.6,12.0,1420000,false
|
||||
2024-01-11,000002.SZ,12.5,12.6,12.1,12.2,12.6,1210000,false
|
||||
2024-01-11,000003.SZ,7.4,7.5,7.2,7.3,7.5,980000,false
|
||||
2024-01-11,600001.SH,15.2,15.2,15.2,15.2,15.2,0,true
|
||||
2024-01-12,000001.SZ,11.5,11.6,11.1,11.2,11.6,1380000,false
|
||||
2024-01-12,000002.SZ,12.1,12.2,11.8,11.9,12.2,1190000,false
|
||||
2024-01-12,000003.SZ,7.2,7.2,6.9,7.0,7.3,960000,false
|
||||
2024-01-12,600001.SH,14.8,15.0,14.7,14.9,15.2,850000,false
|
||||
|
@@ -10,7 +10,7 @@ The roadmap focuses on making the engine complete enough for editable platform
|
||||
strategies, long-range A-share backtests, futures strategies, intraday order
|
||||
simulation, AI-generated strategy code, and service-level result downloads.
|
||||
|
||||
## Re-Audit Findings (2026-04-24)
|
||||
## Re-Audit Findings (2026-08-31)
|
||||
|
||||
The latest re-audit focused on the engine's execution model, account model,
|
||||
order lifecycle, data helper surface, analyzer output, extension hooks, and
|
||||
@@ -20,7 +20,9 @@ futures path. Confirmed aligned areas:
|
||||
pending limit orders, cancellation, open order views, and final order lookup.
|
||||
- Stock account and portfolio runtime fields including cash, frozen cash, total
|
||||
value, transaction cost, trading/position PnL, management fees, financing
|
||||
liability, deposit/withdraw, and position aliases.
|
||||
liability, deposit/withdraw, and position aliases. External deposits and
|
||||
withdrawals are unitized separately from trading PnL; delayed withdrawals
|
||||
are preflighted atomically at settlement.
|
||||
- Scheduler, dynamic universe, subscription guard, `history_bars`,
|
||||
`current_snapshot`, `get_price`, instruments, trading-date APIs, suspension
|
||||
and ST helpers.
|
||||
@@ -37,6 +39,8 @@ futures path. Confirmed aligned areas:
|
||||
| P0 | Futures intraday matching | Closed for daily/open/close, tick-price futures fills, and true multi-level order-book sweeping when optional `order_book_depth` data exists. L1-only data still uses the existing L1 matcher and is not inflated into fake depth. | Extend depth fields only if production vendors expose more levels or exchange-specific fields. |
|
||||
| P0 | Futures open-order lifecycle | Closed for futures pending limit orders, cross-day rematching, cancellation by id/symbol/all, and merged open-order runtime views. | Add more order status transitions only if UI requires extra intermediate event names. |
|
||||
| P0 | Combined multi-account NAV | Closed. `DailyEquityPoint`, progress events, and metrics use aggregate stock + futures initial cash and total equity. | None. |
|
||||
| P0 | Fixed-point execution money | Closed. Stock execution freezes fee rates once and uses signed micro-yuan `i128` for gross amount, commission, stamp tax, transfer fee, strict budget checks, cash, liabilities, management fees, external flows, account units, position lot cost and realized PnL. The standalone futures account uses the same fixed-point money boundary for cash, margin, transaction cost and daily PnL. Market indicators and return statistics remain `f64` outside the execution boundary. | None. |
|
||||
| P0 | Bounded minute-data processing | Closed for the engine data model. Intraday history uses a sorted date index and scans backward only until the requested bar count is satisfied. Daily minute processing consumes a borrowed timestamp-ordered k-way merge and does not clone/materialize the full selected quote day before event dispatch. | Keep Source Lake and service clients batch-streamed; do not reintroduce whole-window row materialization. |
|
||||
| P1 | Futures trading parameter data source | Closed for engine-side trading-parameter ingestion/resolution via `futures_trading_parameters.csv` or component data. | Add more exchange metadata columns only when source data exposes them. |
|
||||
| P1 | Futures transaction cost decider | Closed. `FuturesTransactionCostModel` calculates by-money/by-volume open/close/close-today costs from trading parameters. | None. |
|
||||
| P1 | Futures settlement price mode | Closed. Engine supports configurable settlement price mode and resolves settlement/prev-settlement from factor fields with close/prev_close fallback. | Add dedicated settlement columns if the storage layer later separates them from factors. |
|
||||
@@ -52,22 +56,31 @@ futures path. Confirmed aligned areas:
|
||||
- [x] Rich explicit order styles exposed to platform scripts.
|
||||
- [x] Minute-level `time_rule` semantics including market-open, market-close,
|
||||
and physical-time style schedules.
|
||||
- [x] Fine-grained daily, minute, and tick strategy execution entrypoints.
|
||||
- [x] Fine-grained daily and minute execution quote strategy entrypoints.
|
||||
- [x] Stock broker fee, budget and cash-ledger arithmetic uses a micro-yuan
|
||||
fixed-point execution primitive; one-micro over-budget orders fail.
|
||||
- [x] Stock position lots, realized/unrealized PnL, dividends and external cash
|
||||
flows preserve fixed-point value conservation.
|
||||
- [x] Futures cash, margin, transaction cost and daily realized/position PnL use
|
||||
the fixed-point ledger.
|
||||
- [x] Scheduled actions evaluated against explicit intraday times.
|
||||
- [x] `update_universe`, `subscribe`, and `unsubscribe`.
|
||||
- [x] Tick-frequency subscription guards at strategy API level.
|
||||
- [x] Intraday subscription guards at strategy API level; intraday execution uses minute quote semantics.
|
||||
- [x] VWAP and TWAP explicit action styles.
|
||||
- [x] `order_target_portfolio_smart(..., order_prices=AlgoOrder, valuation_prices=...)`.
|
||||
- [x] Trading PnL, position PnL, dividend receivable, and richer position
|
||||
lifecycle fields.
|
||||
- [x] Stock position aliases including `order_book_id`, `avg_price`,
|
||||
`sellable`, `closable`, `equity`, and `position_prev_close`.
|
||||
- [x] `history_bars` numeric helper for daily, intraday, and tick fields.
|
||||
- [x] `history_bars` numeric helper for daily and minute execution quote fields.
|
||||
- [x] `current_snapshot`, instrument metadata, all-instrument queries, and
|
||||
active/historical instrument helpers.
|
||||
- [x] Trading-date range, previous-date, and next-date helpers.
|
||||
- [x] Phase-aware minute/tick history cursor semantics matching the active bar
|
||||
or tick callback.
|
||||
- [x] Phase-aware minute history cursor semantics matching the active bar or
|
||||
intraday execution quote callback.
|
||||
- [x] Bounded intraday history lookup and borrowed minute quote streaming avoid
|
||||
full-history scans and full-day quote clones while preserving timestamp
|
||||
order and visibility boundaries.
|
||||
- [x] Suspension, ST, date-range price, active instrument, and instrument
|
||||
history helpers.
|
||||
- [x] Open-order status, unfilled quantity, final order lookup, average fill
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"schemaVersion": "fidc-batched-current-rolling-rejection/v1",
|
||||
"measuredAt": "2026-09-05T02:38:00+08:00",
|
||||
"host": "192.168.31.177",
|
||||
"candidateCommit": "004a46c",
|
||||
"revertCommit": "43b15b2098c427869a4a582b4b24325155b1370e",
|
||||
"restoredRunnerBinarySha256": "a4135986b69625a0f3443e9091754874b3f9d65e9913424298c5d8fedf733985",
|
||||
"candidate": {
|
||||
"description": "collect static current rolling windows at strategy construction, batch them per stock, and store fixed current close/volume arrays in StockExpressionState",
|
||||
"processColdEngineSeconds": 6.412,
|
||||
"processHotEngineSeconds": [6.046, 6.497, 6.035, 6.309, 6.074],
|
||||
"processHotMedianEngineSeconds": 6.074
|
||||
},
|
||||
"rollback": {
|
||||
"processColdEngineSeconds": 5.18,
|
||||
"processHotEngineSeconds": [5.48, 5.47, 4.602],
|
||||
"processHotMedianEngineSeconds": 5.47
|
||||
},
|
||||
"observedCandidateRegressionPercent": 11.04204753199269,
|
||||
"businessContract": {
|
||||
"totalReturn": 0.9219861819172002,
|
||||
"tradeCount": 26088,
|
||||
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
|
||||
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
|
||||
"resultConsistent": true,
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"testGate": {
|
||||
"coreUnitTotal": 421,
|
||||
"corePassed": 415,
|
||||
"ignoredManualBenchmarks": 6,
|
||||
"failed": 0,
|
||||
"integrationSuitesPassed": true
|
||||
},
|
||||
"remoteArtifacts": [
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/batched-current-rolling-primary-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/batched-current-rolling-rollback-20260905.json"
|
||||
],
|
||||
"acceptance": {
|
||||
"status": "rejected_and_removed",
|
||||
"reason": "the larger per-stock state and eager batch work cost more than the repeated scalar helper calls on the real five-year workload"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"schemaVersion": "fidc-cached-symbol-board-rejection/v1",
|
||||
"measuredAt": "2026-09-05T03:22:00+08:00",
|
||||
"host": "192.168.31.177",
|
||||
"candidateCommit": "eb8b146",
|
||||
"revertCommit": "f210539",
|
||||
"candidate": {
|
||||
"description": "precompute a symbol-id-aligned BJSE bit vector and share one suffix classifier between platform expressions and risk control",
|
||||
"primaryHotEngineSeconds": [4.656, 4.759, 4.669, 4.698, 4.614],
|
||||
"primaryHotMedianEngineSeconds": 4.669,
|
||||
"acceptedPrimaryBaselineMedianEngineSeconds": 4.729,
|
||||
"primaryObservedImprovementPercent": 1.2687661249735674,
|
||||
"secondaryLowContentionEngineSeconds": [4.404, 4.327, 4.346],
|
||||
"secondaryLowContentionMedianEngineSeconds": 4.346,
|
||||
"acceptedSecondaryBaselineMedianEngineSeconds": 4.049,
|
||||
"secondaryObservedRegressionPercent": 7.33514448011855,
|
||||
"highContentionSecondaryEngineSecondsExcluded": [6.398]
|
||||
},
|
||||
"rollbackComparison": {
|
||||
"primaryEngineSecondsExcluded": [12.736, 12.795, 12.899, 13.845],
|
||||
"hostLoadAverage": 44.84,
|
||||
"reason": "managed symbolic workers entered a roughly 30-core phase, so the rollback batch cannot serve as a same-load wall-time comparison"
|
||||
},
|
||||
"businessContract": {
|
||||
"primaryTotalReturn": 0.9219861819172002,
|
||||
"primaryTradeCount": 26088,
|
||||
"primaryCanonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
|
||||
"primaryResultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
|
||||
"secondaryTotalReturn": 1.1342962298106998,
|
||||
"secondaryTradeCount": 19404,
|
||||
"secondaryCanonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
|
||||
"secondaryResultStoreSha256": "416d2f87241fb4c6b917f6aeecb588f82f6e7d51a103f4c53a74d11971f16839",
|
||||
"resultConsistent": true,
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"profile": {
|
||||
"allThreadRunId": "btr_1788549525151_3404548_13",
|
||||
"allThreadRunEngineSeconds": 4.957,
|
||||
"trimMatchesPercent": 2.0,
|
||||
"typedBaselineSingleWorkerProfileTrimMatchesPercent": 2.85,
|
||||
"comparisonLimited": true,
|
||||
"reason": "the two profiles used different thread attachment sets and cannot establish an end-to-end speedup"
|
||||
},
|
||||
"remoteArtifacts": [
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/cached-symbol-board-primary-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/cached-symbol-board-secondary-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/cached-symbol-board-rollback-primary-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/cached-symbol-board-all-threads-profile-20260905/perf.data"
|
||||
],
|
||||
"acceptance": {
|
||||
"status": "rejected_and_removed",
|
||||
"reason": "the candidate preserved correctness but did not improve both five-year strategies, and the later rollback batch was too heavily contended to overturn the cross-strategy regression"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"schemaVersion": "fidc-calendar-major-series-boundary-index/v1",
|
||||
"measuredAt": "2026-09-05T02:24:00+08:00",
|
||||
"host": "192.168.31.177",
|
||||
"engineCommit": "abe4fed4527e07ad7ae4464e574fa582150e306e",
|
||||
"serviceCommit": "9fd5a9e6d5668af57f6942fc3c4127953545d9c6",
|
||||
"runnerBinarySha256": "a4135986b69625a0f3443e9091754874b3f9d65e9913424298c5d8fedf733985",
|
||||
"implementation": {
|
||||
"description": "transpose immutable decision/current market-series boundary indexes from symbol-major vectors to calendar-major contiguous symbol rows",
|
||||
"logicalEntryCountChanged": false,
|
||||
"entryType": "u32",
|
||||
"missingSentinel": "u32::MAX",
|
||||
"factorValuesCached": false,
|
||||
"selectionResultsCached": false,
|
||||
"pitSemanticsChanged": false
|
||||
},
|
||||
"primaryFiveYearContract": {
|
||||
"startDate": "2021-08-23",
|
||||
"endDate": "2026-08-28",
|
||||
"frequency": "1d",
|
||||
"matchingType": "next_bar_open",
|
||||
"totalReturn": 0.9219861819172002,
|
||||
"tradeCount": 26088,
|
||||
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
|
||||
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
|
||||
"processCold": {
|
||||
"totalSeconds": 18.605,
|
||||
"dataSeconds": 12.849,
|
||||
"dataSetConstructSeconds": 4.052,
|
||||
"marketIndexBuildSeconds": 2.396,
|
||||
"engineSeconds": 5.06
|
||||
},
|
||||
"processHotEngineSeconds": [5.297, 5.227, 5.029, 5.012, 5.202],
|
||||
"processHotMedianEngineSeconds": 5.202,
|
||||
"fieldProjectionBaselineMedianEngineSeconds": 5.356,
|
||||
"observedMedianImprovementPercent": 2.875280059746078,
|
||||
"resultConsistent": true,
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"secondaryFiveYearContract": {
|
||||
"totalReturn": 1.1342962298106998,
|
||||
"tradeCount": 19404,
|
||||
"canonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
|
||||
"resultStoreSha256": "416d2f87241fb4c6b917f6aeecb588f82f6e7d51a103f4c53a74d11971f16839",
|
||||
"engineSeconds": [4.646, 4.87, 4.886, 5.056],
|
||||
"medianEngineSeconds": 4.878,
|
||||
"resultConsistent": true,
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"profile": {
|
||||
"runId": "btr_1788546155152_3120069_10",
|
||||
"engineSeconds": 5.211,
|
||||
"eventCycles": 14556215580,
|
||||
"seriesEndIndexPresentInTopProfile": false,
|
||||
"fieldProjectionBaselineSeriesEndIndexPercent": 4.74,
|
||||
"stockStateSelfPercent": 15.19,
|
||||
"adjustedCloseMovingAveragePercent": 6.24,
|
||||
"numericVmPercent": 6.77
|
||||
},
|
||||
"memory": {
|
||||
"serviceCgroupCurrentBytes": 11493711872,
|
||||
"serviceCgroupPeakBytes": 11495387136,
|
||||
"processRssKiB": 11212504,
|
||||
"processAnonymousKiB": 11196148,
|
||||
"fieldProjectionBaselineCgroupCurrentBytes": 11485425664,
|
||||
"observedCgroupIncreaseBytes": 8286208
|
||||
},
|
||||
"testGate": {
|
||||
"coreUnitTotal": 421,
|
||||
"corePassed": 415,
|
||||
"ignoredManualBenchmarks": 6,
|
||||
"failed": 0,
|
||||
"integrationSuitesPassed": true
|
||||
},
|
||||
"remoteArtifacts": [
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/calendar-major-series-boundary-primary-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/calendar-major-series-boundary-secondary-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/calendar-major-series-boundary-profile-20260905/perf.data",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/calendar-major-series-boundary-profile-20260905/perf-report.txt"
|
||||
],
|
||||
"acceptance": {
|
||||
"status": "accepted_generic_calendar_major_boundary_index",
|
||||
"reason": "both five-year contracts preserve exact outputs and clean terminal audits, the primary hot median improves, the former boundary lookup leaves the top profile, and steady-state memory remains effectively unchanged"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"schemaVersion": "fidc-compact-adjusted-close-rejection/v1",
|
||||
"measuredAt": "2026-09-05T04:29:00+08:00",
|
||||
"host": "192.168.31.177",
|
||||
"candidateCommit": "ab87e18",
|
||||
"revertCommit": "0c2681e6996800eae5f3b881e75a01e7a078863f",
|
||||
"restoredRunnerBinarySha256": "3e69af42e41321d31c69b552cf22d7033ce1ea8d94305a32e32461148cdbfc60",
|
||||
"candidate": {
|
||||
"description": "replace two Vec<Option<f64>> adjusted-close arrays with f64 arrays using NaN as the internal missing sentinel",
|
||||
"theoreticalSteadyStateMemoryReductionBytesPerMarketRow": 16,
|
||||
"hotEngineSeconds": [5.119, 5.168, 5.516, 5.332, 4.69],
|
||||
"hotMedianEngineSeconds": 5.168
|
||||
},
|
||||
"acceptedBaseline": {
|
||||
"historicalMedianEngineSeconds": 3.896,
|
||||
"sameWindowRollbackEngineSeconds": [4.144, 4.169],
|
||||
"sameWindowRollbackMedianEngineSeconds": 4.169
|
||||
},
|
||||
"observed": {
|
||||
"regressionVersusHistoricalBaselinePercent": 32.64887063655031,
|
||||
"regressionVersusSameWindowRollbackPercent": 23.962580954665402
|
||||
},
|
||||
"businessContract": {
|
||||
"totalReturn": 0.9219861819172002,
|
||||
"tradeCount": 26088,
|
||||
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
|
||||
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
|
||||
"resultConsistent": true,
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"testGate": {
|
||||
"coreUnitTotal": 422,
|
||||
"corePassed": 416,
|
||||
"ignoredManualBenchmarks": 6,
|
||||
"failed": 0,
|
||||
"integrationSuitesPassed": true
|
||||
},
|
||||
"remoteArtifacts": [
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/compact-adjusted-close-primary-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/compact-adjusted-close-rollback-primary-20260905.json"
|
||||
],
|
||||
"acceptance": {
|
||||
"status": "rejected_and_removed",
|
||||
"reason": "the memory-dense NaN representation materially slowed the hottest moving-average path despite preserving exact business results"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"schemaVersion": "fidc-compact-daily-stock-state-cache-key/v1",
|
||||
"measuredAt": "2026-09-05T01:20:00+08:00",
|
||||
"host": "192.168.31.177",
|
||||
"engineCommit": "6f81e1940ad4ddd4eefbe12c056be28f2b64d127",
|
||||
"runnerBinarySha256": "c2d9101a89b71b51051b8762375476518e40d40e29a8ee0f9615ac4c55fcdb0a",
|
||||
"implementation": {
|
||||
"description": "remove the redundant execution date from stock-state cache keys because the complete cache is already cleared before the active date changes",
|
||||
"keyBefore": "(execution_date, factor_date, symbol_id, execution_time, use_intraday_quote)",
|
||||
"keyAfter": "(factor_date, symbol_id, execution_time, use_intraday_quote)",
|
||||
"crossDateResetTested": true,
|
||||
"cachedFactorValues": false,
|
||||
"cachedSelectionResults": false,
|
||||
"pitSemanticsChanged": false
|
||||
},
|
||||
"primaryFiveYearContract": {
|
||||
"startDate": "2021-08-23",
|
||||
"endDate": "2026-08-28",
|
||||
"frequency": "1d",
|
||||
"matchingType": "next_bar_open",
|
||||
"totalReturn": 0.9219861819172002,
|
||||
"tradeCount": 26088,
|
||||
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
|
||||
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
|
||||
"hotEngineSeconds": [5.262, 5.734, 5.89, 5.612, 6.301],
|
||||
"hotMedianEngineSeconds": 5.734,
|
||||
"sharedOrderBaselineMedianEngineSeconds": 5.742,
|
||||
"observedMedianImprovementPercent": 0.13932427725531174,
|
||||
"resultConsistent": true,
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"secondaryFiveYearContract": {
|
||||
"totalReturn": 1.1342962298106998,
|
||||
"tradeCount": 19404,
|
||||
"canonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
|
||||
"resultStoreSha256": "416d2f87241fb4c6b917f6aeecb588f82f6e7d51a103f4c53a74d11971f16839",
|
||||
"engineSeconds": [5.889, 5.853, 5.342],
|
||||
"medianEngineSeconds": 5.853,
|
||||
"resultConsistent": true,
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"coldDataObservation": {
|
||||
"runId": "btr_1788542108713_2936456_0",
|
||||
"totalSeconds": 54.502,
|
||||
"dataSeconds": 46.584,
|
||||
"engineSeconds": 7.011,
|
||||
"sourceQuerySeconds": 17.585,
|
||||
"datasetConstructSeconds": 12.913,
|
||||
"marketIndexBuildSeconds": 7.671,
|
||||
"adjustmentValidationSeconds": 1.855,
|
||||
"hotDataSecondsRange": [0.002, 0.006],
|
||||
"performanceComparisonExcluded": true
|
||||
},
|
||||
"memoryObservation": {
|
||||
"processRssKiB": 11136672,
|
||||
"processAnonymousKiB": 11120428,
|
||||
"cgroupCurrentBytes": 12445237248,
|
||||
"cgroupFileCacheBytes": 1029632000,
|
||||
"cgroupActiveFileBytes": 1009012736,
|
||||
"duplicateDataSetLeakObserved": false,
|
||||
"note": "the gap between process RSS and cgroup memory is reclaimable file-page cache rather than a second resident DataSet"
|
||||
},
|
||||
"testGate": {
|
||||
"coreUnitTotal": 420,
|
||||
"corePassed": 414,
|
||||
"ignoredManualBenchmarks": 6,
|
||||
"failed": 0,
|
||||
"integrationSuitesPassed": true
|
||||
},
|
||||
"remoteArtifacts": [
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/compact-stock-cache-key-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/compact-stock-cache-key-secondary-20260905.json"
|
||||
],
|
||||
"acceptance": {
|
||||
"status": "accepted_structural_compaction_no_regression",
|
||||
"endToEndSpeedupClaimed": false,
|
||||
"reason": "the key is smaller, the cross-date isolation test passes, both five-year strategies retain exact results, and observed wall time is neutral to slightly better"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"schemaVersion": "fidc-current-rolling-boundary-reuse-ab/v1",
|
||||
"measuredAt": "2026-09-04T15:17:49Z",
|
||||
"host": "192.168.31.177",
|
||||
"engineCommit": "52b07be19bdd1597e5b412b5a549e57640efa415",
|
||||
"implementation": {
|
||||
"description": "resolve the current market-series end once per stock state and reuse the integer boundary for literal stock rolling_mean_current calls",
|
||||
"cachedFactorValues": false,
|
||||
"cachedStrategyResults": false,
|
||||
"additionalHeapCollectionsPerStock": 0,
|
||||
"fallback": "dynamic or non-market fields retain the original date and factor-series lookup",
|
||||
"pitSemanticsChanged": false,
|
||||
"adjustmentSemanticsChanged": false
|
||||
},
|
||||
"componentBenchmark": {
|
||||
"iterations": 100000,
|
||||
"helperCallsPerIteration": 5,
|
||||
"repeatedLookupSeconds": 0.009142143,
|
||||
"reusedBoundarySeconds": 0.006728638,
|
||||
"speedup": 1.3586914617787433,
|
||||
"equal": true
|
||||
},
|
||||
"testGate": {
|
||||
"total": 418,
|
||||
"passed": 413,
|
||||
"ignoredManualBenchmarks": 5,
|
||||
"failed": 0
|
||||
},
|
||||
"backtestEvidence": {
|
||||
"runId": "btr_1788535029326_2441112_5",
|
||||
"startDate": "2021-08-23",
|
||||
"endDate": "2026-08-28",
|
||||
"frequency": "1d",
|
||||
"matchingType": "next_bar_open",
|
||||
"totalSeconds": 7.509,
|
||||
"engineSeconds": 6.494,
|
||||
"totalReturn": 0.9219861819172002,
|
||||
"tradeCount": 26088,
|
||||
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
|
||||
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54"
|
||||
},
|
||||
"secondaryStrategyEvidence": {
|
||||
"description": "same frozen bundle and execution contract with target positions changed from 30 to 20",
|
||||
"runId": "btr_1788535290995_2441112_6",
|
||||
"totalSeconds": 9.081,
|
||||
"engineSeconds": 8.367,
|
||||
"dataSeconds": 0.003,
|
||||
"totalReturn": 1.1342962298106998,
|
||||
"tradeCount": 19404,
|
||||
"tradeDateCount": 1216,
|
||||
"canonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
|
||||
"resultStoreSha256": "416d2f87241fb4c6b917f6aeecb588f82f6e7d51a103f4c53a74d11971f16839",
|
||||
"matchesPreOptimizationBusinessResult": true
|
||||
},
|
||||
"artifacts": [
|
||||
{
|
||||
"path": "/srv/fidc/canonical/run/fidc-private/evidence/current-rolling-boundary-profile-20260904/perf.data",
|
||||
"sha256": "be9f6e04c01a98cb834bc3f207359dc17555e0e660e178d552b525722a13873a"
|
||||
},
|
||||
{
|
||||
"path": "/srv/fidc/canonical/run/fidc-private/evidence/current-rolling-boundary-profile-20260904/perf-report.txt",
|
||||
"sha256": "6f4e1e4370ce996fcf7308a7a723469b417d8c03156eb43b5a26a5fb42c42511"
|
||||
},
|
||||
{
|
||||
"path": "/srv/fidc/canonical/run/fidc-private/evidence/current-rolling-boundary-profile-20260904/run.json",
|
||||
"sha256": "fee9f104d72d8918cdcd7660d7cd40dced9a84edcabcd68e89588ce5690523a5"
|
||||
}
|
||||
],
|
||||
"acceptance": {
|
||||
"status": "accepted_component_improvement_end_to_end_no_regression",
|
||||
"endToEndSpeedupClaimed": false,
|
||||
"reason": "the component result is positive and exact, while concurrent non-FIDC tan load and symbolic phases make cross-build wall-time medians non-comparable"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"schemaVersion": "fidc-daily-snapshot-view-stock-state-rejection/v1",
|
||||
"measuredAt": "2026-09-05T00:30:00+08:00",
|
||||
"host": "192.168.31.177",
|
||||
"candidateCommit": "1df0081479b93a051f9a40d2acf7060d0eb6929d",
|
||||
"revertCommit": "1f10a6bb3decdd40c77397a7596c6f23a9f90a18",
|
||||
"candidate": {
|
||||
"description": "carry symbol_id in EligibleUniverseSnapshot and pass pre-resolved execution/factor DailySnapshotView values into stock-state construction",
|
||||
"componentBenchmark": {
|
||||
"symbols": 6000,
|
||||
"rounds": 200,
|
||||
"dateMapLookupSeconds": 0.020211,
|
||||
"dailyViewLookupSeconds": 0.006410,
|
||||
"speedup": 3.1530421216848675,
|
||||
"equal": true
|
||||
},
|
||||
"fiveYearRuns": [
|
||||
{"runId": "btr_1788538826285_2693431_0", "engineSeconds": 8.075},
|
||||
{"runId": "btr_1788538854544_2693431_1", "engineSeconds": 8.488},
|
||||
{"runId": "btr_1788538868210_2693431_2", "engineSeconds": 10.455},
|
||||
{"runId": "btr_1788538887159_2693431_3", "engineSeconds": 10.672},
|
||||
{"runId": "btr_1788538903432_2693431_4", "engineSeconds": 11.284},
|
||||
{"runId": "btr_1788538920072_2693431_5", "engineSeconds": 9.985}
|
||||
],
|
||||
"hotMedianEngineSeconds": 10.455,
|
||||
"runnerBinarySha256": "cbe20d1bd5c5e02372bd310107a1f4e41aaa9f256b3c31f79c6ad000648d1854"
|
||||
},
|
||||
"restored": {
|
||||
"fiveYearRuns": [
|
||||
{"runId": "btr_1788539286281_2739751_0", "engineSeconds": 7.118},
|
||||
{"runId": "btr_1788539313845_2739751_1", "engineSeconds": 6.702},
|
||||
{"runId": "btr_1788539325631_2739751_2", "engineSeconds": 7.093},
|
||||
{"runId": "btr_1788539338757_2739751_3", "engineSeconds": 6.509}
|
||||
],
|
||||
"hotMedianEngineSeconds": 6.702,
|
||||
"candidateRegressionPercent": 55.99820948970456,
|
||||
"runnerBinarySha256": "e33dd75aa7b481975c0991cf634d07900cdefa51756802e28e64f18b980a83dc",
|
||||
"matchesPreCandidateRunnerSha256": true
|
||||
},
|
||||
"businessContract": {
|
||||
"startDate": "2021-08-23",
|
||||
"endDate": "2026-08-28",
|
||||
"frequency": "1d",
|
||||
"matchingType": "next_bar_open",
|
||||
"totalReturn": 0.9219861819172002,
|
||||
"tradeCount": 26088,
|
||||
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
|
||||
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
|
||||
"allRunsConsistent": true
|
||||
},
|
||||
"profile": {
|
||||
"candidateRunId": "btr_1788538966659_2693431_6",
|
||||
"candidateEngineSeconds": 10.566,
|
||||
"stockStatePercent": 28.14,
|
||||
"adjustedCloseMovingAveragePercent": 8.27,
|
||||
"stableSortPercent": 5.46,
|
||||
"mallocPercent": 5.12,
|
||||
"note": "The removed map lookup symbols disappeared, but their component savings were too small to offset the larger universe row and generalized optional-view state path."
|
||||
},
|
||||
"remoteArtifacts": [
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/daily-view-after-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/daily-view-profile-20260905/perf.data",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/daily-view-profile-20260905/perf-report.txt",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/daily-view-reverted-20260905.json"
|
||||
],
|
||||
"decision": {
|
||||
"status": "rejected_and_removed",
|
||||
"reason": "a positive lookup microbenchmark did not translate to end-to-end performance; the same frozen five-year workload became about 56 percent slower",
|
||||
"productionEngineCommit": "1f10a6bb3decdd40c77397a7596c6f23a9f90a18"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"schemaVersion": "fidc.date-numeric-vm-performance/v1",
|
||||
"generatedAt": "2026-09-06T18:08:00+08:00",
|
||||
"host": "192.168.31.177",
|
||||
"change": {
|
||||
"engineCommit": "bc228980afc02cde483b1a6fe69da4048120cbc4",
|
||||
"serviceCommit": "bac07c208c652a717d312e332d44f90218cc1247",
|
||||
"runnerBinarySha256": "46684dea71273466f8ba0f6351eb6f6adf694393ff23ee278054e3b6b71acbe5",
|
||||
"description": "Valid ISO date literals directly compared with trade_date, current_date, date, decision_date or execution_date compile to the numeric bytecode VM as YYYYMMDD keys. All other strings remain on the Rhai path. Numeric VM floating comparisons now match Rhai relative epsilon semantics."
|
||||
},
|
||||
"correctnessBug": {
|
||||
"previousBehavior": "Numeric VM used exact f64 comparisons while Rhai used relative f64::EPSILON comparisons, so an expression could change result depending on whether it compiled to VM or fell back to Rhai.",
|
||||
"capturedExample": "11.699999999999998 == 11.7",
|
||||
"previousVmResult": false,
|
||||
"rhaiAndFixedVmResult": true,
|
||||
"operatorsAligned": [
|
||||
"==",
|
||||
"!=",
|
||||
"<",
|
||||
"<=",
|
||||
">",
|
||||
">="
|
||||
]
|
||||
},
|
||||
"request": {
|
||||
"sourceRunId": "btr_1788681250851_3170500_1",
|
||||
"startDate": "2023-01-03",
|
||||
"endDate": "2025-12-31",
|
||||
"frequency": "1d",
|
||||
"matchingType": "next_bar_open",
|
||||
"initialCash": 10000000.0,
|
||||
"benchmark": "000852.SH",
|
||||
"frozenBundleReused": true
|
||||
},
|
||||
"baseline": {
|
||||
"engineCommit": "840473362d7032b80d551ba5935723ee823dffa1",
|
||||
"hotEngineMedianSeconds": 5.049,
|
||||
"hotTotalMedianSeconds": 5.796
|
||||
},
|
||||
"candidate": {
|
||||
"coldRun": {
|
||||
"runId": "btr_1788688964051_3295756_0",
|
||||
"totalSeconds": 21.341,
|
||||
"dataSeconds": 7.904,
|
||||
"engineSeconds": 12.519,
|
||||
"classification": "post-deploy process cold and concurrent host-load observation; retained explicitly and excluded from the hot median"
|
||||
},
|
||||
"hotRunIds": [
|
||||
"btr_1788688990229_3295756_1",
|
||||
"btr_1788689001276_3295756_2",
|
||||
"btr_1788689030863_3295756_3",
|
||||
"btr_1788689059131_3295756_4"
|
||||
],
|
||||
"hotEngineSeconds": [
|
||||
4.963,
|
||||
4.934,
|
||||
4.854,
|
||||
4.859
|
||||
],
|
||||
"hotEngineMedianSeconds": 4.8965,
|
||||
"hotTotalSeconds": [
|
||||
5.901,
|
||||
5.619,
|
||||
5.738,
|
||||
5.539
|
||||
],
|
||||
"hotTotalMedianSeconds": 5.6785,
|
||||
"incrementalHotEngineImprovementPercent": 3.0204,
|
||||
"incrementalHotTotalImprovementPercent": 2.0273
|
||||
},
|
||||
"resultParity": {
|
||||
"totalReturn": -0.615167693429,
|
||||
"tradeCount": 4961,
|
||||
"canonicalResultSha256": "f9c26271d4f2096e51429b6106740f600757823789bf6447cd0c524f702f0836",
|
||||
"resultStoreSha256": "56042bd25bd4261e41e8237c9f5ead580b782d897baae58c8e4436f331a1d91f",
|
||||
"allFiveRunsEqualBaseline": true,
|
||||
"returnDelta": 0.0,
|
||||
"tradeCountDelta": 0
|
||||
},
|
||||
"validation": {
|
||||
"numericVmTests": "4 passed, 1 ignored",
|
||||
"capturedRegressionTest": "platform_helpers_support_generic_rolling_stats_and_normalized_factors",
|
||||
"workspaceTests": "423 passed, 6 ignored plus all integration tests passed",
|
||||
"invalidDateLiteralFallsBack": true,
|
||||
"nonDateStringFallsBack": true,
|
||||
"helperDateArgumentPreserved": true,
|
||||
"runtimeUser": "boris",
|
||||
"serviceHealth": "ok"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"date": "2026-09-08",
|
||||
"host": "192.168.31.177",
|
||||
"candidateCommit": "fa6f189",
|
||||
"unitTestsPassed": 453,
|
||||
"integrationTestsPassed": 122,
|
||||
"ignoredManualBenchmarks": 8,
|
||||
"failed": 0,
|
||||
"backtestRunnerCargoCheck": "passed",
|
||||
"covered": ["top-up rejection", "sell permission preserved", "decision context restored", "existing pending order not rewritten", "next-open target direction determined by actual price"],
|
||||
"deployed": false,
|
||||
"factorCompilerConnected": false,
|
||||
"paperLivePlanConnected": false,
|
||||
"orderAmendmentAcceptanceComplete": false,
|
||||
"brokerAmendmentTestsPassed": true,
|
||||
"amendmentPolicy": "Deny buy quantity increases or limit-price increases; allow validated reductions; preserve original state and queue order on rejection.",
|
||||
"realStrategyAcceptanceComplete": false
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"schemaVersion": "fidc-duplicate-rolling-lookback-rejection/v1",
|
||||
"measuredAt": "2026-09-05T01:05:00+08:00",
|
||||
"host": "192.168.31.177",
|
||||
"candidateCommits": [
|
||||
"1f02e78b2400c73c5f05351b0beff16d6c724e2a",
|
||||
"e469b0ddf4aa3bb9fe69b785b51ce6c7cf960b90"
|
||||
],
|
||||
"revertCommit": "5b2a03d416a7bd709680a1adf52013e3eadc199a",
|
||||
"candidate": {
|
||||
"description": "reuse equal lookback values only inside one AdjustedCloseSeries moving_averages_at_end call",
|
||||
"validComponentBenchmark": {
|
||||
"samples": 6,
|
||||
"iterationsPerSample": 3000000,
|
||||
"lookbacks": [1, 2, 3, 1, 2, 3, 1],
|
||||
"repeatedSeconds": 1.40643611,
|
||||
"reusedSeconds": 0.853281932,
|
||||
"speedup": 1.6482666012902287,
|
||||
"checksum": 1458000000.0
|
||||
},
|
||||
"hardwareCounters": {
|
||||
"runId": "btr_1788541184367_2862338_11",
|
||||
"engineSeconds": 8.423,
|
||||
"taskClockMilliseconds": 9030.56,
|
||||
"cycles": 22931608591,
|
||||
"instructions": 28199702874,
|
||||
"branches": 5010114942,
|
||||
"branchMisses": 23586244,
|
||||
"cacheReferences": 268428800,
|
||||
"cacheMisses": 146632717
|
||||
}
|
||||
},
|
||||
"restoredBaseline": {
|
||||
"warmupRunId": "btr_1788541416661_2890172_0",
|
||||
"warmupEngineSeconds": 5.581,
|
||||
"hardwareCounters": {
|
||||
"runId": "btr_1788541455567_2890172_1",
|
||||
"engineSeconds": 6.078,
|
||||
"taskClockMilliseconds": 6643.43,
|
||||
"cycles": 16773013609,
|
||||
"instructions": 28218847364,
|
||||
"branches": 5024236708,
|
||||
"branchMisses": 23209586,
|
||||
"cacheReferences": 266316407,
|
||||
"cacheMisses": 81226408
|
||||
},
|
||||
"runnerBinarySha256": "188269bf0c2ee65d6c11ed37ddddfdaf86695941e8123f69f950054bea39796e"
|
||||
},
|
||||
"comparison": {
|
||||
"candidateInstructionReductionPercent": 0.06784229696471904,
|
||||
"candidateBranchReductionPercent": 0.2810720756590066,
|
||||
"materialSystemImprovementProven": false,
|
||||
"wallTimeComparable": false,
|
||||
"reason": "host-wide thermal and external workload changes produced materially different cache-miss and clock conditions; the instruction reduction itself is below one tenth of one percent"
|
||||
},
|
||||
"businessContract": {
|
||||
"startDate": "2021-08-23",
|
||||
"endDate": "2026-08-28",
|
||||
"frequency": "1d",
|
||||
"matchingType": "next_bar_open",
|
||||
"totalReturn": 0.9219861819172002,
|
||||
"tradeCount": 26088,
|
||||
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
|
||||
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
|
||||
"allRunsConsistent": true
|
||||
},
|
||||
"remoteArtifacts": [
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/duplicate-lookback-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/duplicate-lookback-low-contention-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/duplicate-lookback-perf-stat-candidate-20260905/perf-stat.csv",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/duplicate-lookback-perf-stat-baseline-20260905/perf-stat.csv"
|
||||
],
|
||||
"decision": {
|
||||
"status": "rejected_and_removed",
|
||||
"reason": "the valid microbenchmark improves, but the real five-year workload saves only about 0.068 percent instructions and provides no measurable system-level benefit",
|
||||
"productionEngineCommit": "5b2a03d416a7bd709680a1adf52013e3eadc199a"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"schemaVersion": "fidc-engine-optimization-rejection/v1",
|
||||
"generatedDate": "2026-09-07",
|
||||
"host": "192.168.31.177",
|
||||
"candidate": {
|
||||
"engineCommit": "757b5665ca01bceb2bf8afe1ae2fe2dfc9c52c5c",
|
||||
"implementationIdentitySha256": "a60aed1a405c1dc4713fdf72a379dcd07bc493998ec92c0aa0573c21ee863fc8",
|
||||
"description": "Keep the flat immutable quote base and merge a run-local lazy quote overlay through every DataSet quote API."
|
||||
},
|
||||
"revert": {
|
||||
"engineCommit": "728ed795b0c3b9bac9b7e79cb90e469c106e3629",
|
||||
"status": "deployed_and_healthy"
|
||||
},
|
||||
"measurements": {
|
||||
"currentCloseOneYear": {
|
||||
"acceptedHotMedianEngineSeconds": 0.722,
|
||||
"candidateHotMedianEngineSeconds": 0.858,
|
||||
"candidateHotMedianTotalSeconds": 1.56,
|
||||
"engineRegressionPercent": 18.836565
|
||||
},
|
||||
"nextBarOpenFiveYear": {
|
||||
"acceptedHotMedianEngineSeconds": 2.223,
|
||||
"candidateHotMedianEngineSeconds": 3.319,
|
||||
"candidateHotMedianTotalSeconds": 4.032,
|
||||
"engineRegressionPercent": 49.302744
|
||||
}
|
||||
},
|
||||
"correctness": {
|
||||
"totalReturnEqual": true,
|
||||
"tradeCountEqual": true,
|
||||
"canonicalResultDigestEqual": true,
|
||||
"resultStoreDigestEqual": true,
|
||||
"terminalAuditEqual": true,
|
||||
"candidateWorkspaceCoreTestsPassed": 442,
|
||||
"revertWorkspaceCoreTestsPassed": 441,
|
||||
"workspaceFailures": 0
|
||||
},
|
||||
"decision": {
|
||||
"status": "rejected_and_removed",
|
||||
"reason": "Public DataSet reads paid overlay branching and enlarged code paths even when no overlay existed; end-to-end regressions outweighed the avoided lazy-write clone.",
|
||||
"replacementDirection": "Build immutable shared Base Panels before engine execution. Any supplemental intraday store must be private to the broker/loader and must not alter generic DataSet read APIs."
|
||||
},
|
||||
"supportingEvidence": [
|
||||
"/Users/boris/WorkSpace/fidc-backtest-service/docs/evidence/current-close-run-overlay-rejection-20260907.json",
|
||||
"/Users/boris/WorkSpace/fidc-backtest-service/docs/evidence/next-open-run-overlay-rejection-20260907.json"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
{
|
||||
"schemaVersion": "fidc.engine-expression-scope-performance/v1",
|
||||
"generatedAt": "2026-09-06T17:45:00+08:00",
|
||||
"host": "192.168.31.177",
|
||||
"profile": {
|
||||
"artifact": "/srv/fidc/canonical/run/fidc-private/evidence/profiles/fidc-hot-engine-d324d284-20260906.data",
|
||||
"event": "cpu-clock",
|
||||
"frequencyHz": 499,
|
||||
"samples": 3501,
|
||||
"lostSamples": 0,
|
||||
"scope": "system.slice/fidc-backtest-service-highmem177.service",
|
||||
"observedSelfOverheadPercent": {
|
||||
"chronoDelayedFormat": 6.31,
|
||||
"chronoStrftimeIterator": 5.0,
|
||||
"selectiveScopePushDynamic": 5.2,
|
||||
"selectiveScopePushString": 3.48,
|
||||
"selectiveScopePushInteger": 3.03,
|
||||
"selectiveScopePushBoolean": 1.09,
|
||||
"marketBySymbolId": 6.2,
|
||||
"malloc": 4.74,
|
||||
"free": 2.43
|
||||
}
|
||||
},
|
||||
"change": {
|
||||
"baselineEngineCommit": "630a7a1",
|
||||
"candidateEngineCommit": "840473362d7032b80d551ba5935723ee823dffa1",
|
||||
"serviceCommit": "bac07c208c652a717d312e332d44f90218cc1247",
|
||||
"behavior": "Expression scope identifiers are passed directly to Rhai without an intermediate heap String. Trade, decision and execution date strings are formatted once per DayExpressionState and reused as ImmutableString values by every expression evaluation on that day."
|
||||
},
|
||||
"request": {
|
||||
"sourceRunId": "btr_1788681250851_3170500_1",
|
||||
"startDate": "2023-01-03",
|
||||
"endDate": "2025-12-31",
|
||||
"frequency": "1d",
|
||||
"matchingType": "next_bar_open",
|
||||
"initialCash": 10000000.0,
|
||||
"benchmark": "000852.SH",
|
||||
"frozenBundleReused": true
|
||||
},
|
||||
"baseline": {
|
||||
"runIds": [
|
||||
"btr_1788686878533_3170500_3",
|
||||
"btr_1788686907702_3170500_4",
|
||||
"btr_1788687153269_3170500_5"
|
||||
],
|
||||
"engineSeconds": [
|
||||
6.582,
|
||||
6.442,
|
||||
6.352
|
||||
],
|
||||
"engineMedianSeconds": 6.442,
|
||||
"totalSeconds": [
|
||||
7.22,
|
||||
7.04,
|
||||
7.388
|
||||
],
|
||||
"totalMedianSeconds": 7.22
|
||||
},
|
||||
"candidate": {
|
||||
"coldRun": {
|
||||
"runId": "btr_1788687746484_3277755_0",
|
||||
"totalSeconds": 13.369,
|
||||
"dataSeconds": 7.827,
|
||||
"engineSeconds": 4.925,
|
||||
"classification": "runner_process_cold"
|
||||
},
|
||||
"hotRunIds": [
|
||||
"btr_1788687763649_3277755_1",
|
||||
"btr_1788687774308_3277755_2",
|
||||
"btr_1788687798961_3277755_3",
|
||||
"btr_1788687813292_3277755_4"
|
||||
],
|
||||
"hotEngineSeconds": [
|
||||
5.021,
|
||||
5.077,
|
||||
5.18,
|
||||
4.951
|
||||
],
|
||||
"hotEngineMedianSeconds": 5.049,
|
||||
"hotTotalSeconds": [
|
||||
5.659,
|
||||
5.756,
|
||||
5.836,
|
||||
5.84
|
||||
],
|
||||
"hotTotalMedianSeconds": 5.796,
|
||||
"engineMedianImprovementPercent": 21.624,
|
||||
"totalMedianImprovementPercent": 19.723
|
||||
},
|
||||
"correctness": {
|
||||
"totalReturn": -0.615167693429,
|
||||
"tradeCount": 4961,
|
||||
"canonicalResultSha256": "f9c26271d4f2096e51429b6106740f600757823789bf6447cd0c524f702f0836",
|
||||
"resultStoreSha256": "56042bd25bd4261e41e8237c9f5ead580b782d897baae58c8e4436f331a1d91f",
|
||||
"allFiveCandidateRunsEqualBaseline": true,
|
||||
"returnDelta": 0.0,
|
||||
"tradeCountDelta": 0
|
||||
},
|
||||
"validation": {
|
||||
"cargoFmt": "passed",
|
||||
"platformExpressionTests": "180 passed, 1 ignored",
|
||||
"workspaceTests": "421 passed, 6 ignored plus all integration tests passed",
|
||||
"runtimeUser": "boris",
|
||||
"serviceHealth": "ok",
|
||||
"buildManifest": "/srv/fidc/canonical/run/build/fidc-backtest-service.json",
|
||||
"runnerBinarySha256": "909cb47ef20128b4990bf355d0c63e10d7d74aac128067fa19c971698c5d85bc"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"schemaVersion": "fidc-frozen-standard-rolling-lookbacks-rejection/v1",
|
||||
"measuredAt": "2026-09-05T03:03:00+08:00",
|
||||
"host": "192.168.31.177",
|
||||
"candidateCommit": "32e4030",
|
||||
"revertCommit": "05e67c73df0c002efec15711dd5ef066d817abec",
|
||||
"restoredRunnerBinarySha256": "1071852a374620029398a967d485b827ed2defbb6ecbcc3f1ee6ffd3e2db76b6",
|
||||
"candidate": {
|
||||
"description": "replace per-stock rolling requirement-set lookups with strategy-level fixed standard lookback arrays",
|
||||
"primaryHotEngineSeconds": [4.329, 4.455, 4.751, 4.822, 4.847],
|
||||
"primaryHotMedianEngineSeconds": 4.751,
|
||||
"secondaryEngineSeconds": [4.774, 5.473, 6.161, 5.491],
|
||||
"hardwareRunId": "btr_1788548442183_3221468_10",
|
||||
"hardwareRunEngineSeconds": 10.522,
|
||||
"hardwareRunHostLoadAverage": 42.93,
|
||||
"instructions": 26384933876,
|
||||
"branches": 4682344116
|
||||
},
|
||||
"acceptedBaseline": {
|
||||
"engineCommit": "75ab0c0",
|
||||
"primaryHotMedianEngineSeconds": 4.729,
|
||||
"instructions": 26049740736,
|
||||
"branches": 4601262679
|
||||
},
|
||||
"observed": {
|
||||
"primaryMedianRegressionPercent": 0.4652146331148285,
|
||||
"instructionRegressionPercent": 1.2867427104054538,
|
||||
"branchRegressionPercent": 1.7621562309418415
|
||||
},
|
||||
"businessContract": {
|
||||
"primaryTotalReturn": 0.9219861819172002,
|
||||
"primaryTradeCount": 26088,
|
||||
"primaryCanonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
|
||||
"primaryResultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
|
||||
"secondaryTotalReturn": 1.1342962298106998,
|
||||
"secondaryTradeCount": 19404,
|
||||
"secondaryCanonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
|
||||
"secondaryResultStoreSha256": "416d2f87241fb4c6b917f6aeecb588f82f6e7d51a103f4c53a74d11971f16839",
|
||||
"resultConsistent": true,
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"remoteArtifacts": [
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/frozen-standard-rolling-lookbacks-primary-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/frozen-standard-rolling-lookbacks-secondary-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/frozen-standard-rolling-lookbacks-perf-stat-20260905/perf-stat.csv"
|
||||
],
|
||||
"acceptance": {
|
||||
"status": "rejected_and_removed",
|
||||
"reason": "the compiler layout change increased retired instructions and branches while the primary end-to-end median did not improve"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"schemaVersion": "fidc-instrument-symbol-id-index-ab/v1",
|
||||
"measuredAt": "2026-09-04T14:31:25Z",
|
||||
"host": "192.168.31.177",
|
||||
"engineCommit": "8ed22897eac506297c9ab4023140fbb6cfa0cc26",
|
||||
"serviceCommit": "9fd5a9e6d5668af57f6942fc3c4127953545d9c6",
|
||||
"serviceBinarySha256": "24b53421e8724a29963e3254d9dea33c0a2259a81567caa853b98369df7cf704",
|
||||
"runnerBinarySha256": "0d314809675544fed19ba0888ae22e0098f87da1672ac692fe38f3dbd3b55c70",
|
||||
"implementation": {
|
||||
"description": "build one immutable Instrument vector aligned to the existing symbol-id index and use it only at call sites that already own a validated symbol id",
|
||||
"stringInstrumentApiPreserved": true,
|
||||
"mutableStateShared": false,
|
||||
"riskSemanticsChanged": false,
|
||||
"executionSemanticsChanged": false
|
||||
},
|
||||
"componentBenchmark": {
|
||||
"rows": 6000,
|
||||
"iterations": 1000,
|
||||
"hashMapSeconds": 0.09067713,
|
||||
"denseVectorSeconds": 0.010526148,
|
||||
"speedup": 8.614464664566752,
|
||||
"checksum": 600000000,
|
||||
"equal": true
|
||||
},
|
||||
"testGate": {
|
||||
"total": 417,
|
||||
"passed": 413,
|
||||
"ignoredManualBenchmarks": 4,
|
||||
"failed": 0
|
||||
},
|
||||
"backtestContract": {
|
||||
"bundleId": "bt_bundle_5baf27b98346123e1a7b",
|
||||
"bundleHash": "68571cc2c3082cacf1774e681d6ff82d1beddb4241cebc2179f9235001cd730f",
|
||||
"startDate": "2021-08-23",
|
||||
"endDate": "2026-08-28",
|
||||
"frequency": "1d",
|
||||
"matchingType": "next_bar_open",
|
||||
"totalReturn": 0.9219861819172002,
|
||||
"tradeCount": 26088,
|
||||
"tradeDateCount": 1216,
|
||||
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
|
||||
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54"
|
||||
},
|
||||
"before": {
|
||||
"runtime": "5dec29a0d6ee",
|
||||
"lowOverheadEngineSeconds": [7.363, 6.922, 6.917],
|
||||
"medianEngineSeconds": 6.922
|
||||
},
|
||||
"after": {
|
||||
"processColdRun": {
|
||||
"runId": "btr_1788533379797_2361496_0",
|
||||
"totalSeconds": 20.534,
|
||||
"dataSeconds": 13.02,
|
||||
"engineSeconds": 6.377
|
||||
},
|
||||
"hotRuns": [
|
||||
{"runId": "btr_1788533414497_2361496_1", "totalSeconds": 7.182, "engineSeconds": 6.517},
|
||||
{"runId": "btr_1788533424201_2361496_2", "totalSeconds": 7.164, "engineSeconds": 6.482},
|
||||
{"runId": "btr_1788533436894_2361496_3", "totalSeconds": 7.289, "engineSeconds": 6.48},
|
||||
{"runId": "btr_1788533447093_2361496_4", "totalSeconds": 7.367, "engineSeconds": 6.477}
|
||||
],
|
||||
"medianHotEngineSeconds": 6.481,
|
||||
"medianEngineImprovementPercent": 6.37,
|
||||
"lowOverheadProfileRun": {
|
||||
"runId": "btr_1788533478319_2361496_5",
|
||||
"totalSeconds": 6.917,
|
||||
"engineSeconds": 6.251,
|
||||
"sampleCount": 332,
|
||||
"lostSamples": 0
|
||||
},
|
||||
"serviceMemoryCurrentBytes": 11426889728,
|
||||
"serviceMemoryPeakBytes": 11434971136
|
||||
},
|
||||
"artifacts": [
|
||||
{
|
||||
"path": "/srv/fidc/canonical/run/fidc-private/evidence/instrument-symbol-id-profile-20260904/hot-five-year-perf.data",
|
||||
"sha256": "a3d788f8e6ad799604b83019a4403fee509d6c955606f46bd721960e3d38433b"
|
||||
},
|
||||
{
|
||||
"path": "/srv/fidc/canonical/run/fidc-private/evidence/instrument-symbol-id-profile-20260904/hot-five-year-perf-report.txt",
|
||||
"sha256": "dbd3004b6ab2719e67b34ca4c6d98f908053f6f28f617bd1c1969552b0f14291"
|
||||
},
|
||||
{
|
||||
"path": "/srv/fidc/canonical/run/fidc-private/evidence/instrument-symbol-id-profile-20260904/hot-five-year-run.json",
|
||||
"sha256": "092276999525532005a815ab9d2588ee8865b4652c11367ca9ffe52b6a601557"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"schemaVersion": "fidc-interned-stock-state-symbols/v1",
|
||||
"measuredAt": "2026-09-05T01:50:00+08:00",
|
||||
"host": "192.168.31.177",
|
||||
"engineCommit": "e5646ef80c9a5c6d2d3a1b9b6f330fbbc27aa4ab",
|
||||
"runnerBinarySha256": "c2f6f0b8cc194a5e3f668bcc0d54b3712cf037267ccc6c04d5f850d8ccbd6314",
|
||||
"implementation": {
|
||||
"description": "intern each canonical symbol once in DataSet and clone Arc<str> into per-date StockExpressionState objects instead of allocating a String for every state",
|
||||
"rhaiStringBoundaryPreserved": true,
|
||||
"crossDateSharedSymbolTested": true,
|
||||
"cachedFactorValues": false,
|
||||
"cachedSelectionResults": false,
|
||||
"pitSemanticsChanged": false
|
||||
},
|
||||
"primaryFiveYearContract": {
|
||||
"startDate": "2021-08-23",
|
||||
"endDate": "2026-08-28",
|
||||
"frequency": "1d",
|
||||
"matchingType": "next_bar_open",
|
||||
"totalReturn": 0.9219861819172002,
|
||||
"tradeCount": 26088,
|
||||
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
|
||||
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
|
||||
"lowContentionEngineSeconds": [5.505, 5.748, 5.67, 6.005, 6.497],
|
||||
"medianEngineSeconds": 5.748,
|
||||
"compactKeyBaselineMedianEngineSeconds": 5.734,
|
||||
"resultConsistent": true,
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"secondaryFiveYearContract": {
|
||||
"totalReturn": 1.1342962298106998,
|
||||
"tradeCount": 19404,
|
||||
"canonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
|
||||
"resultStoreSha256": "416d2f87241fb4c6b917f6aeecb588f82f6e7d51a103f4c53a74d11971f16839",
|
||||
"engineSeconds": [5.648, 5.35, 5.003],
|
||||
"medianEngineSeconds": 5.35,
|
||||
"resultConsistent": true,
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"hardwareCounters": {
|
||||
"candidateRunId": "btr_1788544043073_3041540_6",
|
||||
"candidateEngineSeconds": 8.227,
|
||||
"candidateInstructions": 27997441984,
|
||||
"candidateBranches": 4947191270,
|
||||
"earlierSharedIndexBaselineInstructions": 28218847364,
|
||||
"earlierSharedIndexBaselineBranches": 5024236708,
|
||||
"observedInstructionReductionPercent": 0.7846044523842924,
|
||||
"observedBranchReductionPercent": 1.533451728719079,
|
||||
"attributionNote": "the earlier hardware baseline predates the compact cache-key change, so counters prove the combined structural reduction rather than symbol interning alone"
|
||||
},
|
||||
"profile": {
|
||||
"runId": "btr_1788544253262_3041540_15",
|
||||
"totalSeconds": 6.136,
|
||||
"engineSeconds": 5.509,
|
||||
"eventCycles": 15296376746,
|
||||
"stockStatePercent": 16.25,
|
||||
"preInternStockStatePercent": 23.86,
|
||||
"mallocPercent": 4.22,
|
||||
"numericVmPercent": 6.02,
|
||||
"adjustedCloseMovingAveragePercent": 5.10
|
||||
},
|
||||
"excludedHighContentionBatch": {
|
||||
"hostLoadAveragePeak": 69.23,
|
||||
"engineSeconds": [7.247, 7.094, 7.481, 7.967, 37.981, 44.366],
|
||||
"resultConsistent": true,
|
||||
"performanceComparisonExcluded": true,
|
||||
"reason": "independent tan child processes executed on backtest-reserved CPUs; no external process was modified"
|
||||
},
|
||||
"memory": {
|
||||
"serviceCgroupCurrentBytes": 11485425664,
|
||||
"serviceCgroupPeakBytes": 11488174080,
|
||||
"processRssKiB": 11196756,
|
||||
"processAnonymousKiB": 11180328
|
||||
},
|
||||
"testGate": {
|
||||
"coreUnitTotal": 420,
|
||||
"corePassed": 414,
|
||||
"ignoredManualBenchmarks": 6,
|
||||
"failed": 0,
|
||||
"integrationSuitesPassed": true
|
||||
},
|
||||
"remoteArtifacts": [
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/interned-stock-symbol-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/interned-stock-symbol-low-contention-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/interned-stock-symbol-secondary-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/interned-stock-symbol-perf-stat-20260905/perf-stat.csv",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/interned-stock-symbol-profile-20260905/perf.data",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/interned-stock-symbol-profile-20260905/perf-report.txt"
|
||||
],
|
||||
"acceptance": {
|
||||
"status": "accepted_structural_allocation_reduction_no_regression",
|
||||
"endToEndSpeedupClaimed": false,
|
||||
"reason": "both five-year strategies preserve exact outputs, low-contention wall time is neutral, and the stock-state profile plus hardware counters show less allocation-related work"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"schemaVersion": "fidc-engine-optimization-rejection/v1",
|
||||
"generatedDate": "2026-09-07",
|
||||
"host": "192.168.31.177",
|
||||
"candidate": {
|
||||
"engineCommit": "f2de8b281a7f9537665d86895f8c51fb33386d9e",
|
||||
"implementationIdentitySha256": "1ea0d2834901504e79d76d9df722f009a2ced3c2a02ddf247eae8722be135516",
|
||||
"description": "Store execution quote dates, symbol maps and quote vectors as nested Arc values so a lazy write clones only one path."
|
||||
},
|
||||
"revert": {
|
||||
"engineCommit": "68c186f649965118da63ff0f0dc2272005538a55",
|
||||
"implementationIdentitySha256": "4f81eb707baeb76375a03eb8989cb043089b1a7c1d07a998570861eb7dc6fa95",
|
||||
"description": "Restore the flat read-optimized execution quote index."
|
||||
},
|
||||
"measurements": {
|
||||
"currentCloseOneYear": {
|
||||
"candidateHotMedianTotalSeconds": 1.19,
|
||||
"candidateHotMedianEngineSeconds": 0.652,
|
||||
"revertHotMedianTotalSeconds": 1.183,
|
||||
"revertHotMedianEngineSeconds": 0.722,
|
||||
"candidateEngineImprovementPercent": 9.695291
|
||||
},
|
||||
"nextBarOpenFiveYear": {
|
||||
"candidateHotMedianTotalSeconds": 2.896,
|
||||
"candidateHotMedianEngineSeconds": 2.25,
|
||||
"revertHotMedianTotalSeconds": 2.848,
|
||||
"revertHotMedianEngineSeconds": 2.223,
|
||||
"candidateEngineRegressionPercent": 1.214575
|
||||
},
|
||||
"staticCurrentClose": {
|
||||
"candidateHotMedianEngineSeconds": 0.324,
|
||||
"acceptedFlatIndexHotMedianEngineSeconds": 0.322
|
||||
},
|
||||
"fullMinute": {
|
||||
"candidateHotMedianEngineSeconds": 0.225,
|
||||
"acceptedFlatIndexHotMedianEngineSeconds": 0.226
|
||||
}
|
||||
},
|
||||
"correctness": {
|
||||
"totalReturnEqual": true,
|
||||
"tradeCountEqual": true,
|
||||
"canonicalResultDigestEqual": true,
|
||||
"resultStoreDigestEqual": true,
|
||||
"terminalAuditEqual": true,
|
||||
"candidateWorkspaceCoreTestsPassed": 442,
|
||||
"revertWorkspaceCoreTestsPassed": 441,
|
||||
"workspaceFailures": 0
|
||||
},
|
||||
"decision": {
|
||||
"status": "rejected_and_removed",
|
||||
"reason": "Nested Arc indirection improves the lazy-write strategy but causes a repeatable next_bar_open regression and has no static/full-minute read benefit.",
|
||||
"replacementDirection": "Keep the flat immutable base index and isolate lazy additions in a run-local overlay without changing the base iterator representation."
|
||||
},
|
||||
"supportingEvidence": [
|
||||
"/Users/boris/WorkSpace/fidc-backtest-service/docs/evidence/current-close-layered-quote-index-rejection-20260907.json",
|
||||
"/Users/boris/WorkSpace/fidc-backtest-service/docs/evidence/static-pool-layered-quote-index-rejection-20260907.json",
|
||||
"/Users/boris/WorkSpace/fidc-backtest-service/docs/evidence/full-minute-layered-quote-index-rejection-20260907.json",
|
||||
"/Users/boris/WorkSpace/fidc-backtest-service/docs/evidence/next-open-layered-quote-index-rejection-20260907.json",
|
||||
"/Users/boris/WorkSpace/fidc-backtest-service/docs/evidence/current-close-after-layered-index-revert-20260907.json",
|
||||
"/Users/boris/WorkSpace/fidc-backtest-service/docs/evidence/next-open-after-layered-index-revert-20260907.json"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"schemaVersion": "fidc-lazy-expression-scope-rejection/v1",
|
||||
"measuredAt": "2026-09-05T04:06:00+08:00",
|
||||
"host": "192.168.31.177",
|
||||
"candidateCommit": "1b44928",
|
||||
"revertCommit": "229ca8332df050c6961f73bd1a17de0dab583cf0",
|
||||
"restoredRunnerBinarySha256": "3e69af42e41321d31c69b552cf22d7033ce1ea8d94305a32e32461148cdbfc60",
|
||||
"candidate": {
|
||||
"description": "add closure-based lazy SelectiveExpressionScope bindings for dates, order/process state, stock strings, and position availability",
|
||||
"primaryHotEngineSeconds": [3.838, 3.867, 4.017, 3.931, 3.866],
|
||||
"primaryHotMedianEngineSeconds": 3.867,
|
||||
"secondaryEngineSeconds": [3.664, 3.649, 3.693, 3.767],
|
||||
"secondaryMedianEngineSeconds": 3.693
|
||||
},
|
||||
"acceptedBaseline": {
|
||||
"primaryMedianEngineSeconds": 3.896,
|
||||
"secondaryMedianEngineSeconds": 3.582
|
||||
},
|
||||
"observed": {
|
||||
"primaryImprovementPercent": 0.744353182751538,
|
||||
"secondaryRegressionPercent": 3.098827470686773
|
||||
},
|
||||
"rollbackBatchExcluded": {
|
||||
"secondaryEngineSeconds": [5.343, 5.352, 6.996],
|
||||
"hostLoadAverage": 28.38,
|
||||
"reason": "managed factor workload changed phase after deployment; this batch is retained for correctness only"
|
||||
},
|
||||
"businessContract": {
|
||||
"primaryTotalReturn": 0.9219861819172002,
|
||||
"primaryTradeCount": 26088,
|
||||
"primaryCanonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
|
||||
"primaryResultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
|
||||
"secondaryTotalReturn": 1.1342962298106998,
|
||||
"secondaryTradeCount": 19404,
|
||||
"secondaryCanonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
|
||||
"secondaryResultStoreSha256": "416d2f87241fb4c6b917f6aeecb588f82f6e7d51a103f4c53a74d11971f16839",
|
||||
"resultConsistent": true,
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"testGate": {
|
||||
"coreUnitTotal": 423,
|
||||
"corePassed": 417,
|
||||
"ignoredManualBenchmarks": 6,
|
||||
"failed": 0,
|
||||
"integrationSuitesPassed": true
|
||||
},
|
||||
"remoteArtifacts": [
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/lazy-expression-scope-primary-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/lazy-expression-scope-secondary-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/lazy-expression-scope-rollback-secondary-20260905.json"
|
||||
],
|
||||
"acceptance": {
|
||||
"status": "rejected_and_removed",
|
||||
"reason": "closure-based lazy bindings marginally helped one strategy but regressed the second stable five-year contract, so the implementation was removed"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"schemaVersion": "fidc-engine-optimization-ab/v1",
|
||||
"generatedDate": "2026-09-07",
|
||||
"host": "192.168.31.177",
|
||||
"change": {
|
||||
"engineCommit": "df7a1ee382843b22d145718576d24e78fdd6c804",
|
||||
"description": "Use binary partition points on the shared ascending market-cap symbol index before constructing per-stock expression state.",
|
||||
"activation": "Only ascending market-cap rank with a market-cap selection field and finite inclusive bounds.",
|
||||
"fallback": "Free-float, custom field, custom expression, descending rank and non-finite bounds keep the original full candidate path.",
|
||||
"cachedBusinessResults": false
|
||||
},
|
||||
"deployment": {
|
||||
"serviceCommit": "e4ed9467b9a4ca3ed56c64724623f0ba0d005b91",
|
||||
"implementationIdentitySha256": "b38dc2bb32d46197eb2c48460757b7e4e1c1a3ea80dee1447a169d35af4e759e",
|
||||
"serviceBinarySha256": "31b97e2ff8d9dca00c685ab7e70c239cb038ba467ed3ec669a237de7b8d0ba27",
|
||||
"runnerBinarySha256": "932d531a222df936c82ca38c3ddad1f940c386e315c906cd3da4f428348114d8",
|
||||
"serviceUser": "boris",
|
||||
"sourceLakeRestarted": false,
|
||||
"health": "ok"
|
||||
},
|
||||
"currentCloseOneYear": {
|
||||
"before": {
|
||||
"processHotMedianTotalSeconds": 1.238,
|
||||
"processHotMedianEngineSeconds": 0.773
|
||||
},
|
||||
"after": {
|
||||
"runs": 5,
|
||||
"processHotMedianTotalSeconds": 1.154,
|
||||
"processHotMedianEngineSeconds": 0.714,
|
||||
"totalReturn": 0.3201517861398,
|
||||
"tradeCount": 5351,
|
||||
"canonicalResultDigest": "7204c6f41b8e39fbf1af7fc55cd601b80f3427a7aa058394ccd8b0b14ca48eed",
|
||||
"resultStoreDigest": "0b4d24ed5ec2b27cc4135707b4c51c78eb2c3e35a20da8108610778c30c72c73",
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"improvementPercent": {
|
||||
"total": 6.785137,
|
||||
"engine": 7.6326
|
||||
}
|
||||
},
|
||||
"crossModeRegression": {
|
||||
"staticCurrentClose": {
|
||||
"runs": 6,
|
||||
"processHotMedianTotalSeconds": 0.822,
|
||||
"processHotMedianEngineSeconds": 0.322,
|
||||
"totalReturn": 0.13228843240310018,
|
||||
"tradeCount": 4445,
|
||||
"canonicalResultDigest": "fdfa855295c0b55bdbe6f39952ead1515e844bf033ced974d3c3ddc037a5d0b1",
|
||||
"resultStoreDigest": "697566645116c76ff837cd36f7f9bbd7ad3eb30510a5b95012fb730d5072d511"
|
||||
},
|
||||
"nextBarOpenFiveYear": {
|
||||
"runs": 5,
|
||||
"processHotMedianTotalSeconds": 2.811,
|
||||
"processHotMedianEngineSeconds": 2.188,
|
||||
"totalReturn": 0.9922618879291,
|
||||
"tradeCount": 25827,
|
||||
"canonicalResultDigest": "ac1d167cb1e1073e1d1ecb01e914f94d7560081c1d238e6b4418d86233250719",
|
||||
"resultStoreDigest": "79570e0ae6b07badc1b693dc897dd1381647d259a4fe44ed3e50bc215e2fd088"
|
||||
},
|
||||
"fullMinute": {
|
||||
"runs": 6,
|
||||
"processHotMedianTotalSeconds": 0.598,
|
||||
"processHotMedianEngineSeconds": 0.226,
|
||||
"totalReturn": 0.03473222656500008,
|
||||
"tradeCount": 156,
|
||||
"canonicalResultDigest": "7dae3a618932b90d36e9968c027f08a94d69b8fc6a56b1ea0cc1a2cb771d85b8",
|
||||
"resultStoreDigest": "bbbd7080b8fd7f6e8c3a8132499842bd0e4d644dbfdc3e8dbca7d2c0c1381e93"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"beforePath": "/srv/fidc/canonical/run/fidc-private/evidence/current-close-shared-quote-profile-20260907",
|
||||
"afterPath": "/srv/fidc/canonical/run/fidc-private/evidence/current-close-market-cap-range-profile-20260907",
|
||||
"beforeSamples": 408,
|
||||
"afterSamples": 375,
|
||||
"lostSamples": 0,
|
||||
"stockStatePercentBefore": 6.8,
|
||||
"stockStatePercentAfter": 3.02,
|
||||
"marketCapSelectionPercentBefore": 2.91,
|
||||
"marketCapSelectionPercentAfter": 1.7
|
||||
},
|
||||
"verification": {
|
||||
"workspaceCoreTestsPassed": 441,
|
||||
"workspaceCoreTestsIgnored": 6,
|
||||
"workspaceFailures": 0,
|
||||
"allCanonicalDigestsMatched": true,
|
||||
"allResultStoreDigestsMatched": true,
|
||||
"allTerminalAuditsClean": true
|
||||
},
|
||||
"supportingEvidence": [
|
||||
"/Users/boris/WorkSpace/fidc-backtest-service/docs/evidence/current-close-market-cap-range-20260907.json",
|
||||
"/Users/boris/WorkSpace/fidc-backtest-service/docs/evidence/static-pool-market-cap-range-control-20260907.json",
|
||||
"/Users/boris/WorkSpace/fidc-backtest-service/docs/evidence/next-open-market-cap-range-20260907.json",
|
||||
"/Users/boris/WorkSpace/fidc-backtest-service/docs/evidence/full-minute-market-cap-range-control-20260907.json"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,467 @@
|
||||
{
|
||||
"schemaVersion": "fidc-backtest-benchmark/v5",
|
||||
"label": "native-pit-intraday",
|
||||
"generatedAtUnixSeconds": 1788790043,
|
||||
"requestSha256": "9dd420ae606878b90ff660fcd4efe520e04a237ec758ed974b87b6d9a72cab73",
|
||||
"requestOrigin": {
|
||||
"kind": "request_file"
|
||||
},
|
||||
"baseline": null,
|
||||
"baselineComparable": true,
|
||||
"baselineComparison": null,
|
||||
"bundleRefresh": null,
|
||||
"baseUrl": "http://127.0.0.1:8081",
|
||||
"runs": [
|
||||
{
|
||||
"slot": 0,
|
||||
"cacheClass": "restart_or_cold",
|
||||
"runId": "btr_1788790021780_1150210_0",
|
||||
"status": "succeeded",
|
||||
"queueWaitSeconds": 0.0,
|
||||
"executionSeconds": 8.994,
|
||||
"totalSeconds": 8.994,
|
||||
"runnerSeconds": 7.904,
|
||||
"bundleValidationSeconds": 0.166,
|
||||
"sourceEpochSeconds": 0.0,
|
||||
"strategyContractSeconds": 0.001,
|
||||
"terminalCalendarSeconds": 0.0,
|
||||
"riskFreeRateSeconds": 0.031,
|
||||
"riskFreeRateCacheHit": false,
|
||||
"engineSeconds": 0.012,
|
||||
"dataSeconds": 7.662,
|
||||
"resultSeconds": 0.03,
|
||||
"unattributedSeconds": 0.0,
|
||||
"canonicalSeconds": 0.001,
|
||||
"resultFinalization": {
|
||||
"canonicalSeconds": 0.001,
|
||||
"processEventStoreSeconds": 0.001,
|
||||
"storePublishSeconds": 0.027,
|
||||
"summarySeconds": 0.0
|
||||
},
|
||||
"dataPreparationTimings": {
|
||||
"adjustmentValidationSeconds": 0.0,
|
||||
"availableStockStDates": 0,
|
||||
"benchmarkFetchSeconds": 0.344,
|
||||
"candidatePlanBasePanelSeconds": 0.006,
|
||||
"candidatePlanSelectionSeconds": 0.004,
|
||||
"corporateActionSeconds": 0.605,
|
||||
"datasetConstructSeconds": 0.0,
|
||||
"externalFetchSeconds": 0.344,
|
||||
"historyDateSeconds": 0.0,
|
||||
"instrumentIndexBuildSeconds": 0.0,
|
||||
"instrumentMetadataSeconds": 0.016,
|
||||
"loopSeconds": 0.0,
|
||||
"marketIndexBuildSeconds": 0.001,
|
||||
"quotePlanSeconds": 0.935,
|
||||
"riskSnapshotSeconds": 0.0,
|
||||
"sourceQuerySeconds": 5.752,
|
||||
"stockStLookupSeconds": 0.0,
|
||||
"supplementalExecutionFetchSeconds": 0.0,
|
||||
"totalSeconds": 7.659
|
||||
},
|
||||
"finalizationSeconds": 1.089,
|
||||
"finalizationTimings": {
|
||||
"artifactVerificationSeconds": 0.0,
|
||||
"cacheRegistrationSeconds": 0.0,
|
||||
"postgresClientAcquireSeconds": 0.216,
|
||||
"postgresPersistSeconds": 0.872,
|
||||
"preTerminalUpdateSeconds": 1.089,
|
||||
"schemaCheckSeconds": 0.0,
|
||||
"timingSource": "fidc-backtest-service-monotonic"
|
||||
},
|
||||
"totalReturn": 0.011211658838299776,
|
||||
"tradeCount": 104,
|
||||
"riskDecisionCount": 11,
|
||||
"resultStoreDigest": "5ed069a1f5999542e6da78b10797aa21aabe71e13dd79649cd11157cbc57d3b4",
|
||||
"canonicalResultDigest": "5c8a110cc6f285b9d569e818a0472a8b5c76f14df853a1c2c42d1b5222c39b3a",
|
||||
"terminalAudit": {
|
||||
"cashReceivableCount": 0,
|
||||
"cashReceivableTotalAmount": 0.0,
|
||||
"earliestDeferredCashDate": null,
|
||||
"futuresOpenOrderCount": 0,
|
||||
"lastExecutionDate": "2025-09-12",
|
||||
"omittedOpenOrderCount": 0,
|
||||
"openOrderSamples": [],
|
||||
"pendingCashFlowCount": 0,
|
||||
"pendingCashFlowNetAmount": 0.0,
|
||||
"status": "clean",
|
||||
"stockOpenOrderCount": 0
|
||||
},
|
||||
"terminalAuditVerified": true,
|
||||
"terminalAuditSha256": "5836794e260aa2cfd1a06d4568ae6577a37c9cbe09ec0f00995121ea5b2522d9",
|
||||
"implementation": {
|
||||
"buildManifestSha256": "fb4f7ac191c8ab8d8d39fc48014f84ce01d4c0731ffe986e7d5cb0107a3e5539",
|
||||
"builtAt": "2026-09-07T22:06:18+08:00",
|
||||
"engineCommit": "1b78186c4e37273c64c1f9e208c2e47021ee4fff",
|
||||
"identitySha256": "14e7ff10848d64ffcec232dea8cff23a032702301b81ae036bb0b26b4b0b5e33",
|
||||
"runnerBinarySha256": "7cddebf9ab5cbbd5d6f9bd5c7bf006c3895ee26967d23d80dc86b47c50fb761d",
|
||||
"schemaVersion": "fidc-backtest-implementation/v1",
|
||||
"serviceBinarySha256": "f09d121d73c34ad3cd5e2e0c24b0bdaf1974309c9ec3b93229cfb824b1e9f11a",
|
||||
"serviceCommit": "c80a38b91a398fd713f1f1faaa19a581ea85efad",
|
||||
"status": "verified"
|
||||
},
|
||||
"implementationVerified": true,
|
||||
"implementationIdentitySha256": "14e7ff10848d64ffcec232dea8cff23a032702301b81ae036bb0b26b4b0b5e33",
|
||||
"artifactManifest": {
|
||||
"canonicalResult": {
|
||||
"algorithm": "sha256",
|
||||
"ordering": "engine_fact_order_v2",
|
||||
"schemaVersion": "fidc-canonical-backtest-result/v2",
|
||||
"sections": {
|
||||
"accountEvents": {
|
||||
"rowCount": 104,
|
||||
"sha256": "ea6bbbd55df3da0692dfd712f88ef6f8af510ada318641ede71884f5c5d14e7e"
|
||||
},
|
||||
"equityFacts": {
|
||||
"rowCount": 5,
|
||||
"sha256": "96004e235e4603c0a7d1a5629a2f0d2b364258d562e1d397cdd6925718a7d060"
|
||||
},
|
||||
"fillEvents": {
|
||||
"rowCount": 104,
|
||||
"sha256": "2c37f626b7de014f5638dc8edf33f3ca8d23126ab2e62f26f01fbcb33e8c4e48"
|
||||
},
|
||||
"holdingSnapshots": {
|
||||
"rowCount": 112,
|
||||
"sha256": "50abab8a304d2e2a7de344538aff604c8bf96ef00283330d107916cc2e234f67"
|
||||
},
|
||||
"orderEvents": {
|
||||
"rowCount": 108,
|
||||
"sha256": "f327f21151a5d7dbe53d2dcde9202be699f9672f758373eeb9c67985ca3826f5"
|
||||
},
|
||||
"riskAudits": {
|
||||
"rowCount": 11,
|
||||
"sha256": "428a8d7395b54ec5a9523eb745d5007c64b9d9867615e870d95ea9ea176f536e"
|
||||
}
|
||||
},
|
||||
"sha256": "5c8a110cc6f285b9d569e818a0472a8b5c76f14df853a1c2c42d1b5222c39b3a",
|
||||
"totalRows": 444
|
||||
},
|
||||
"processEventStore": {
|
||||
"bytes": 44302,
|
||||
"identitySha256": "1444075abea626e8d7d1b8c9c9cda984a2a687ff2b9465db5648e0f4525130e7",
|
||||
"manifestBytes": 461,
|
||||
"manifestSha256": "b43a818096d09648b809a46d12ad9eae39638d57ea52f7696906359036a80c52",
|
||||
"schemaVersion": "fidc-backtest-process-event-manifest/v1",
|
||||
"sections": {
|
||||
"processEvents": {
|
||||
"blockCount": 1,
|
||||
"bytes": 43841,
|
||||
"rowCount": 370,
|
||||
"sha256": "8edb6dad95777e3eef5946acbbdfc4b9125df34a79ac716d8c3ff7835b552bf6"
|
||||
}
|
||||
},
|
||||
"sha256": "661d84b7b430ac25fb5c959df8e7fb309133784d85a7524375cd803e29e52094",
|
||||
"totalEvents": 370
|
||||
},
|
||||
"resultStore": {
|
||||
"bytes": 148006,
|
||||
"identitySha256": "484b7a4a8d5cc77c6222ce01f2fe452c890995a476579f209d1d4ac43885d60a",
|
||||
"manifestBytes": 2999,
|
||||
"manifestSha256": "a0a7072f176db79d781846cc124d2e4794c5084c48c525876863751f709c4e7f",
|
||||
"schemaVersion": "fidc-backtest-fact-manifest/v1",
|
||||
"sections": {
|
||||
"accountEvents": {
|
||||
"blockCount": 1,
|
||||
"bytes": 13313,
|
||||
"rowCount": 104,
|
||||
"sha256": "ab2f936c9072f80d58487b62a6f64553d279582ddfe6af0299afe20447e83424"
|
||||
},
|
||||
"equityFacts": {
|
||||
"blockCount": 1,
|
||||
"bytes": 11009,
|
||||
"rowCount": 5,
|
||||
"sha256": "c40650f5b27cfff873194572ffe28a5412fe77730fe75492c7b5a575f8f5caae"
|
||||
},
|
||||
"fillEvents": {
|
||||
"blockCount": 1,
|
||||
"bytes": 40541,
|
||||
"rowCount": 104,
|
||||
"sha256": "80a3f1d3a9c23db7dec2dc70f029280c0844370be6d87703cd396a234068f708"
|
||||
},
|
||||
"holdingSnapshots": {
|
||||
"blockCount": 1,
|
||||
"bytes": 51338,
|
||||
"rowCount": 112,
|
||||
"sha256": "7684244dc8e7788f777309169be61b14fcb532abdd89fe93f0a7f2980a615ba9"
|
||||
},
|
||||
"orderEvents": {
|
||||
"blockCount": 1,
|
||||
"bytes": 25768,
|
||||
"rowCount": 108,
|
||||
"sha256": "31ff15d2cf79b1eb9c333d19d8fb400c9c5a52c36b0f662330ec39aa9d0f805c"
|
||||
},
|
||||
"riskAudits": {
|
||||
"blockCount": 1,
|
||||
"bytes": 3038,
|
||||
"rowCount": 11,
|
||||
"sha256": "5a7269219a21cf14c23d788a6a96ddf1d1ddf0ef1e3b57a8db4ae4b1da789aff"
|
||||
}
|
||||
},
|
||||
"sha256": "5ed069a1f5999542e6da78b10797aa21aabe71e13dd79649cd11157cbc57d3b4",
|
||||
"totalEvents": 444
|
||||
},
|
||||
"schemaVersion": "fidc-backtest-artifacts/v4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"slot": 1,
|
||||
"cacheClass": "process_hot",
|
||||
"runId": "btr_1788790036494_1150210_1",
|
||||
"status": "succeeded",
|
||||
"queueWaitSeconds": 0.0,
|
||||
"executionSeconds": 0.596,
|
||||
"totalSeconds": 0.596,
|
||||
"runnerSeconds": 0.026,
|
||||
"bundleValidationSeconds": 0.0,
|
||||
"sourceEpochSeconds": 0.0,
|
||||
"strategyContractSeconds": 0.003,
|
||||
"terminalCalendarSeconds": 0.0,
|
||||
"riskFreeRateSeconds": 0.0,
|
||||
"riskFreeRateCacheHit": true,
|
||||
"engineSeconds": 0.012,
|
||||
"dataSeconds": 0.007,
|
||||
"resultSeconds": 0.002,
|
||||
"unattributedSeconds": 0.0,
|
||||
"canonicalSeconds": 0.001,
|
||||
"resultFinalization": {
|
||||
"canonicalSeconds": 0.001,
|
||||
"processEventStoreSeconds": 0.001,
|
||||
"storePublishSeconds": 0.0,
|
||||
"summarySeconds": 0.0
|
||||
},
|
||||
"dataPreparationTimings": {
|
||||
"adjustmentValidationSeconds": 0.0,
|
||||
"availableStockStDates": 0,
|
||||
"benchmarkFetchSeconds": 0.0,
|
||||
"candidatePlanBasePanelSeconds": 0.0,
|
||||
"candidatePlanSelectionSeconds": 0.0,
|
||||
"corporateActionSeconds": 0.0,
|
||||
"datasetConstructSeconds": 0.0,
|
||||
"externalFetchSeconds": 0.0,
|
||||
"historyDateSeconds": 0.0,
|
||||
"instrumentIndexBuildSeconds": 0.0,
|
||||
"instrumentMetadataSeconds": 0.0,
|
||||
"loopSeconds": 0.0,
|
||||
"marketIndexBuildSeconds": 0.0,
|
||||
"quotePlanSeconds": 0.0,
|
||||
"riskSnapshotSeconds": 0.0,
|
||||
"sourceQuerySeconds": 0.0,
|
||||
"stockStLookupSeconds": 0.0,
|
||||
"supplementalExecutionFetchSeconds": 0.0,
|
||||
"totalSeconds": 0.006
|
||||
},
|
||||
"finalizationSeconds": 0.569,
|
||||
"finalizationTimings": {
|
||||
"artifactVerificationSeconds": 0.001,
|
||||
"cacheRegistrationSeconds": 0.0,
|
||||
"postgresClientAcquireSeconds": 0.27,
|
||||
"postgresPersistSeconds": 0.298,
|
||||
"preTerminalUpdateSeconds": 0.569,
|
||||
"schemaCheckSeconds": 0.0,
|
||||
"timingSource": "fidc-backtest-service-monotonic"
|
||||
},
|
||||
"totalReturn": 0.011211658838299776,
|
||||
"tradeCount": 104,
|
||||
"riskDecisionCount": 11,
|
||||
"resultStoreDigest": "5ed069a1f5999542e6da78b10797aa21aabe71e13dd79649cd11157cbc57d3b4",
|
||||
"canonicalResultDigest": "5c8a110cc6f285b9d569e818a0472a8b5c76f14df853a1c2c42d1b5222c39b3a",
|
||||
"terminalAudit": {
|
||||
"cashReceivableCount": 0,
|
||||
"cashReceivableTotalAmount": 0.0,
|
||||
"earliestDeferredCashDate": null,
|
||||
"futuresOpenOrderCount": 0,
|
||||
"lastExecutionDate": "2025-09-12",
|
||||
"omittedOpenOrderCount": 0,
|
||||
"openOrderSamples": [],
|
||||
"pendingCashFlowCount": 0,
|
||||
"pendingCashFlowNetAmount": 0.0,
|
||||
"status": "clean",
|
||||
"stockOpenOrderCount": 0
|
||||
},
|
||||
"terminalAuditVerified": true,
|
||||
"terminalAuditSha256": "5836794e260aa2cfd1a06d4568ae6577a37c9cbe09ec0f00995121ea5b2522d9",
|
||||
"implementation": {
|
||||
"buildManifestSha256": "fb4f7ac191c8ab8d8d39fc48014f84ce01d4c0731ffe986e7d5cb0107a3e5539",
|
||||
"builtAt": "2026-09-07T22:06:18+08:00",
|
||||
"engineCommit": "1b78186c4e37273c64c1f9e208c2e47021ee4fff",
|
||||
"identitySha256": "14e7ff10848d64ffcec232dea8cff23a032702301b81ae036bb0b26b4b0b5e33",
|
||||
"runnerBinarySha256": "7cddebf9ab5cbbd5d6f9bd5c7bf006c3895ee26967d23d80dc86b47c50fb761d",
|
||||
"schemaVersion": "fidc-backtest-implementation/v1",
|
||||
"serviceBinarySha256": "f09d121d73c34ad3cd5e2e0c24b0bdaf1974309c9ec3b93229cfb824b1e9f11a",
|
||||
"serviceCommit": "c80a38b91a398fd713f1f1faaa19a581ea85efad",
|
||||
"status": "verified"
|
||||
},
|
||||
"implementationVerified": true,
|
||||
"implementationIdentitySha256": "14e7ff10848d64ffcec232dea8cff23a032702301b81ae036bb0b26b4b0b5e33",
|
||||
"artifactManifest": {
|
||||
"canonicalResult": {
|
||||
"algorithm": "sha256",
|
||||
"ordering": "engine_fact_order_v2",
|
||||
"schemaVersion": "fidc-canonical-backtest-result/v2",
|
||||
"sections": {
|
||||
"accountEvents": {
|
||||
"rowCount": 104,
|
||||
"sha256": "ea6bbbd55df3da0692dfd712f88ef6f8af510ada318641ede71884f5c5d14e7e"
|
||||
},
|
||||
"equityFacts": {
|
||||
"rowCount": 5,
|
||||
"sha256": "96004e235e4603c0a7d1a5629a2f0d2b364258d562e1d397cdd6925718a7d060"
|
||||
},
|
||||
"fillEvents": {
|
||||
"rowCount": 104,
|
||||
"sha256": "2c37f626b7de014f5638dc8edf33f3ca8d23126ab2e62f26f01fbcb33e8c4e48"
|
||||
},
|
||||
"holdingSnapshots": {
|
||||
"rowCount": 112,
|
||||
"sha256": "50abab8a304d2e2a7de344538aff604c8bf96ef00283330d107916cc2e234f67"
|
||||
},
|
||||
"orderEvents": {
|
||||
"rowCount": 108,
|
||||
"sha256": "f327f21151a5d7dbe53d2dcde9202be699f9672f758373eeb9c67985ca3826f5"
|
||||
},
|
||||
"riskAudits": {
|
||||
"rowCount": 11,
|
||||
"sha256": "428a8d7395b54ec5a9523eb745d5007c64b9d9867615e870d95ea9ea176f536e"
|
||||
}
|
||||
},
|
||||
"sha256": "5c8a110cc6f285b9d569e818a0472a8b5c76f14df853a1c2c42d1b5222c39b3a",
|
||||
"totalRows": 444
|
||||
},
|
||||
"processEventStore": {
|
||||
"bytes": 44302,
|
||||
"identitySha256": "1444075abea626e8d7d1b8c9c9cda984a2a687ff2b9465db5648e0f4525130e7",
|
||||
"manifestBytes": 461,
|
||||
"manifestSha256": "b43a818096d09648b809a46d12ad9eae39638d57ea52f7696906359036a80c52",
|
||||
"schemaVersion": "fidc-backtest-process-event-manifest/v1",
|
||||
"sections": {
|
||||
"processEvents": {
|
||||
"blockCount": 1,
|
||||
"bytes": 43841,
|
||||
"rowCount": 370,
|
||||
"sha256": "8edb6dad95777e3eef5946acbbdfc4b9125df34a79ac716d8c3ff7835b552bf6"
|
||||
}
|
||||
},
|
||||
"sha256": "661d84b7b430ac25fb5c959df8e7fb309133784d85a7524375cd803e29e52094",
|
||||
"totalEvents": 370
|
||||
},
|
||||
"resultStore": {
|
||||
"bytes": 148006,
|
||||
"identitySha256": "484b7a4a8d5cc77c6222ce01f2fe452c890995a476579f209d1d4ac43885d60a",
|
||||
"manifestBytes": 2999,
|
||||
"manifestSha256": "a0a7072f176db79d781846cc124d2e4794c5084c48c525876863751f709c4e7f",
|
||||
"schemaVersion": "fidc-backtest-fact-manifest/v1",
|
||||
"sections": {
|
||||
"accountEvents": {
|
||||
"blockCount": 1,
|
||||
"bytes": 13313,
|
||||
"rowCount": 104,
|
||||
"sha256": "ab2f936c9072f80d58487b62a6f64553d279582ddfe6af0299afe20447e83424"
|
||||
},
|
||||
"equityFacts": {
|
||||
"blockCount": 1,
|
||||
"bytes": 11009,
|
||||
"rowCount": 5,
|
||||
"sha256": "c40650f5b27cfff873194572ffe28a5412fe77730fe75492c7b5a575f8f5caae"
|
||||
},
|
||||
"fillEvents": {
|
||||
"blockCount": 1,
|
||||
"bytes": 40541,
|
||||
"rowCount": 104,
|
||||
"sha256": "80a3f1d3a9c23db7dec2dc70f029280c0844370be6d87703cd396a234068f708"
|
||||
},
|
||||
"holdingSnapshots": {
|
||||
"blockCount": 1,
|
||||
"bytes": 51338,
|
||||
"rowCount": 112,
|
||||
"sha256": "7684244dc8e7788f777309169be61b14fcb532abdd89fe93f0a7f2980a615ba9"
|
||||
},
|
||||
"orderEvents": {
|
||||
"blockCount": 1,
|
||||
"bytes": 25768,
|
||||
"rowCount": 108,
|
||||
"sha256": "31ff15d2cf79b1eb9c333d19d8fb400c9c5a52c36b0f662330ec39aa9d0f805c"
|
||||
},
|
||||
"riskAudits": {
|
||||
"blockCount": 1,
|
||||
"bytes": 3038,
|
||||
"rowCount": 11,
|
||||
"sha256": "5a7269219a21cf14c23d788a6a96ddf1d1ddf0ef1e3b57a8db4ae4b1da789aff"
|
||||
}
|
||||
},
|
||||
"sha256": "5ed069a1f5999542e6da78b10797aa21aabe71e13dd79649cd11157cbc57d3b4",
|
||||
"totalEvents": 444
|
||||
},
|
||||
"schemaVersion": "fidc-backtest-artifacts/v4"
|
||||
}
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"resultConsistent": true,
|
||||
"resultStoreDigest": "5ed069a1f5999542e6da78b10797aa21aabe71e13dd79649cd11157cbc57d3b4",
|
||||
"canonicalResultDigest": "5c8a110cc6f285b9d569e818a0472a8b5c76f14df853a1c2c42d1b5222c39b3a",
|
||||
"totalReturn": 0.011211658838299776,
|
||||
"tradeCount": 104,
|
||||
"terminalAudit": {
|
||||
"cashReceivableCount": 0,
|
||||
"cashReceivableTotalAmount": 0.0,
|
||||
"earliestDeferredCashDate": null,
|
||||
"futuresOpenOrderCount": 0,
|
||||
"lastExecutionDate": "2025-09-12",
|
||||
"omittedOpenOrderCount": 0,
|
||||
"openOrderSamples": [],
|
||||
"pendingCashFlowCount": 0,
|
||||
"pendingCashFlowNetAmount": 0.0,
|
||||
"status": "clean",
|
||||
"stockOpenOrderCount": 0
|
||||
},
|
||||
"terminalAuditSha256": "5836794e260aa2cfd1a06d4568ae6577a37c9cbe09ec0f00995121ea5b2522d9",
|
||||
"implementation": {
|
||||
"buildManifestSha256": "fb4f7ac191c8ab8d8d39fc48014f84ce01d4c0731ffe986e7d5cb0107a3e5539",
|
||||
"builtAt": "2026-09-07T22:06:18+08:00",
|
||||
"engineCommit": "1b78186c4e37273c64c1f9e208c2e47021ee4fff",
|
||||
"identitySha256": "14e7ff10848d64ffcec232dea8cff23a032702301b81ae036bb0b26b4b0b5e33",
|
||||
"runnerBinarySha256": "7cddebf9ab5cbbd5d6f9bd5c7bf006c3895ee26967d23d80dc86b47c50fb761d",
|
||||
"schemaVersion": "fidc-backtest-implementation/v1",
|
||||
"serviceBinarySha256": "f09d121d73c34ad3cd5e2e0c24b0bdaf1974309c9ec3b93229cfb824b1e9f11a",
|
||||
"serviceCommit": "c80a38b91a398fd713f1f1faaa19a581ea85efad",
|
||||
"status": "verified"
|
||||
},
|
||||
"implementationIdentitySha256": "14e7ff10848d64ffcec232dea8cff23a032702301b81ae036bb0b26b4b0b5e33",
|
||||
"restartOrCold": {
|
||||
"queueWaitSeconds": 0.0,
|
||||
"executionSeconds": 8.994,
|
||||
"totalSeconds": 8.994,
|
||||
"runnerSeconds": 7.904,
|
||||
"bundleValidationSeconds": 0.166,
|
||||
"sourceEpochSeconds": 0.0,
|
||||
"strategyContractSeconds": 0.001,
|
||||
"terminalCalendarSeconds": 0.0,
|
||||
"riskFreeRateSeconds": 0.031,
|
||||
"riskFreeRateCacheHit": false,
|
||||
"engineSeconds": 0.012,
|
||||
"dataSeconds": 7.662,
|
||||
"resultSeconds": 0.03,
|
||||
"unattributedSeconds": 0.0,
|
||||
"canonicalSeconds": 0.001,
|
||||
"finalizationSeconds": 1.089
|
||||
},
|
||||
"processHotMedian": {
|
||||
"queueWaitSeconds": 0.0,
|
||||
"executionSeconds": 0.596,
|
||||
"totalSeconds": 0.596,
|
||||
"runnerSeconds": 0.026,
|
||||
"bundleValidationSeconds": 0.0,
|
||||
"sourceEpochSeconds": 0.0,
|
||||
"strategyContractSeconds": 0.003,
|
||||
"terminalCalendarSeconds": 0.0,
|
||||
"riskFreeRateSeconds": 0.0,
|
||||
"riskFreeRateCacheHit": true,
|
||||
"engineSeconds": 0.012,
|
||||
"dataSeconds": 0.007,
|
||||
"resultSeconds": 0.002,
|
||||
"unattributedSeconds": 0.0,
|
||||
"canonicalSeconds": 0.001,
|
||||
"finalizationSeconds": 0.569
|
||||
}
|
||||
}
|
||||
}
|
||||
+531
File diff suppressed because one or more lines are too long
@@ -0,0 +1,276 @@
|
||||
{
|
||||
"schemaVersion": "fidc-backtest-benchmark/v5",
|
||||
"label": "native-pit-nextopen",
|
||||
"generatedAtUnixSeconds": 1788790370,
|
||||
"requestSha256": "7948497bfa966eb118e56438e7c261869a26ab70a62186b92f761719f85e91c7",
|
||||
"requestOrigin": {
|
||||
"kind": "request_file"
|
||||
},
|
||||
"baseline": null,
|
||||
"baselineComparable": true,
|
||||
"baselineComparison": null,
|
||||
"bundleRefresh": null,
|
||||
"baseUrl": "http://127.0.0.1:8081",
|
||||
"runs": [
|
||||
{
|
||||
"slot": 0,
|
||||
"cacheClass": "restart_or_cold",
|
||||
"runId": "btr_1788790344805_1150210_2",
|
||||
"status": "succeeded",
|
||||
"queueWaitSeconds": 0.0,
|
||||
"executionSeconds": 21.61,
|
||||
"totalSeconds": 21.61,
|
||||
"runnerSeconds": 21.294,
|
||||
"bundleValidationSeconds": 0.004,
|
||||
"sourceEpochSeconds": 0.0,
|
||||
"strategyContractSeconds": 0.003,
|
||||
"terminalCalendarSeconds": 0.0,
|
||||
"riskFreeRateSeconds": 0.017,
|
||||
"riskFreeRateCacheHit": false,
|
||||
"engineSeconds": 5.566,
|
||||
"dataSeconds": 15.305,
|
||||
"resultSeconds": 0.395,
|
||||
"unattributedSeconds": 0.0,
|
||||
"canonicalSeconds": 0.206,
|
||||
"resultFinalization": {
|
||||
"canonicalSeconds": 0.206,
|
||||
"processEventStoreSeconds": 0.168,
|
||||
"storePublishSeconds": 0.005,
|
||||
"summarySeconds": 0.003
|
||||
},
|
||||
"dataPreparationTimings": {
|
||||
"adjustmentValidationSeconds": 0.0,
|
||||
"availableStockStDates": 0,
|
||||
"benchmarkFetchSeconds": 0.023,
|
||||
"candidatePlanBasePanelSeconds": 0.0,
|
||||
"candidatePlanSelectionSeconds": 0.0,
|
||||
"corporateActionSeconds": 0.017,
|
||||
"datasetConstructSeconds": 2.943,
|
||||
"externalFetchSeconds": 0.025,
|
||||
"historyDateSeconds": 0.0,
|
||||
"instrumentIndexBuildSeconds": 0.269,
|
||||
"instrumentMetadataSeconds": 0.006,
|
||||
"loopSeconds": 0.925,
|
||||
"marketIndexBuildSeconds": 1.846,
|
||||
"quotePlanSeconds": 0.0,
|
||||
"riskSnapshotSeconds": 0.0,
|
||||
"sourceQuerySeconds": 9.0,
|
||||
"stockStLookupSeconds": 0.0,
|
||||
"supplementalExecutionFetchSeconds": 0.0,
|
||||
"totalSeconds": 15.3
|
||||
},
|
||||
"finalizationSeconds": 0.307,
|
||||
"finalizationTimings": {
|
||||
"artifactVerificationSeconds": 0.008,
|
||||
"cacheRegistrationSeconds": 0.0,
|
||||
"postgresClientAcquireSeconds": 0.0,
|
||||
"postgresPersistSeconds": 0.299,
|
||||
"preTerminalUpdateSeconds": 0.307,
|
||||
"schemaCheckSeconds": 0.0,
|
||||
"timingSource": "fidc-backtest-service-monotonic"
|
||||
},
|
||||
"totalReturn": 0.9857987122635,
|
||||
"tradeCount": 25408,
|
||||
"riskDecisionCount": 113,
|
||||
"resultStoreDigest": "1eab5a5ffe59efeaba6b2048a33760aab31d42efa0fc10e35a4a7048db853235",
|
||||
"canonicalResultDigest": "b29b085d43bcc0f8f1712767421781c70570a24112933623d4bbbef46508d710",
|
||||
"terminalAudit": {
|
||||
"cashReceivableCount": 0,
|
||||
"cashReceivableTotalAmount": 0.0,
|
||||
"earliestDeferredCashDate": null,
|
||||
"futuresOpenOrderCount": 0,
|
||||
"lastExecutionDate": "2026-08-28",
|
||||
"omittedOpenOrderCount": 0,
|
||||
"openOrderSamples": [],
|
||||
"pendingCashFlowCount": 0,
|
||||
"pendingCashFlowNetAmount": 0.0,
|
||||
"status": "clean",
|
||||
"stockOpenOrderCount": 0
|
||||
},
|
||||
"terminalAuditVerified": true,
|
||||
"terminalAuditSha256": "0e8503c2bfe9ecfd93bab3a95f79461830886eb693cfe6f477279908c0d95bd5",
|
||||
"implementation": {
|
||||
"buildManifestSha256": "fb4f7ac191c8ab8d8d39fc48014f84ce01d4c0731ffe986e7d5cb0107a3e5539",
|
||||
"builtAt": "2026-09-07T22:06:18+08:00",
|
||||
"engineCommit": "1b78186c4e37273c64c1f9e208c2e47021ee4fff",
|
||||
"identitySha256": "14e7ff10848d64ffcec232dea8cff23a032702301b81ae036bb0b26b4b0b5e33",
|
||||
"runnerBinarySha256": "7cddebf9ab5cbbd5d6f9bd5c7bf006c3895ee26967d23d80dc86b47c50fb761d",
|
||||
"schemaVersion": "fidc-backtest-implementation/v1",
|
||||
"serviceBinarySha256": "f09d121d73c34ad3cd5e2e0c24b0bdaf1974309c9ec3b93229cfb824b1e9f11a",
|
||||
"serviceCommit": "c80a38b91a398fd713f1f1faaa19a581ea85efad",
|
||||
"status": "verified"
|
||||
},
|
||||
"implementationVerified": true,
|
||||
"implementationIdentitySha256": "14e7ff10848d64ffcec232dea8cff23a032702301b81ae036bb0b26b4b0b5e33",
|
||||
"artifactManifest": {
|
||||
"canonicalResult": {
|
||||
"algorithm": "sha256",
|
||||
"ordering": "engine_fact_order_v2",
|
||||
"schemaVersion": "fidc-canonical-backtest-result/v2",
|
||||
"sections": {
|
||||
"accountEvents": {
|
||||
"rowCount": 25572,
|
||||
"sha256": "da7576c1cbade9b48f1a154d07e870f1472924f4e0054cba0ef06739585ce380"
|
||||
},
|
||||
"equityFacts": {
|
||||
"rowCount": 1216,
|
||||
"sha256": "13db0a6d484cdf871e4f1dc1a4bc8db1e809d2808b21cf4fcab3961e6ad9e6fe"
|
||||
},
|
||||
"fillEvents": {
|
||||
"rowCount": 25408,
|
||||
"sha256": "92e0d103bbb487af45d27b720cdc92ef398d16e11655a6d0bcff8046013335db"
|
||||
},
|
||||
"holdingSnapshots": {
|
||||
"rowCount": 33164,
|
||||
"sha256": "97e3d60230a167a960bd2f615adfa33a9171527b62f1a181e2d5af694090e492"
|
||||
},
|
||||
"orderEvents": {
|
||||
"rowCount": 25541,
|
||||
"sha256": "635fe79eab9b1db0c116da2860ded66c350c051f44751d36306d158777039744"
|
||||
},
|
||||
"riskAudits": {
|
||||
"rowCount": 113,
|
||||
"sha256": "17bc940bcac18b050235ae3f17195f12586d1e29cd7bc64b6189edf2e07d1406"
|
||||
}
|
||||
},
|
||||
"sha256": "b29b085d43bcc0f8f1712767421781c70570a24112933623d4bbbef46508d710",
|
||||
"totalRows": 111014
|
||||
},
|
||||
"processEventStore": {
|
||||
"bytes": 10115947,
|
||||
"identitySha256": "e89d00ea1c2525c2ab1b57a29f2447047f2523083a953869ea2e25a488986565",
|
||||
"manifestBytes": 9757,
|
||||
"manifestSha256": "9bff5b97488c8804275ee65a717f01dca9ac68443b9766d4aab8d36189b1d8a1",
|
||||
"schemaVersion": "fidc-backtest-process-event-manifest/v1",
|
||||
"sections": {
|
||||
"processEvents": {
|
||||
"blockCount": 61,
|
||||
"bytes": 10106190,
|
||||
"rowCount": 83859,
|
||||
"sha256": "f436a32403aaa74b3068a0d8c74bfb7c429523d3f78617234eec82a009ab3da2"
|
||||
}
|
||||
},
|
||||
"sha256": "aa7488614a85f1527713332039b0fa212d1807f11b03e0e701480bf5d8ede1a4",
|
||||
"totalEvents": 83859
|
||||
},
|
||||
"resultStore": {
|
||||
"bytes": 35831197,
|
||||
"identitySha256": "a238986d82354e802d2c73526d035021b38e1eed0a2b0b0f1eb6ce41ed4f321b",
|
||||
"manifestBytes": 54237,
|
||||
"manifestSha256": "0991f096db161fd16da00a7529ece54a8cf68643d88aeec8f4d3fedc6c740d03",
|
||||
"schemaVersion": "fidc-backtest-fact-manifest/v1",
|
||||
"sections": {
|
||||
"accountEvents": {
|
||||
"blockCount": 61,
|
||||
"bytes": 3303444,
|
||||
"rowCount": 25572,
|
||||
"sha256": "276fd3edd7e60afbb05fa32c111db3e1df2025637887509b6353b24c7f127a48"
|
||||
},
|
||||
"equityFacts": {
|
||||
"blockCount": 61,
|
||||
"bytes": 3761920,
|
||||
"rowCount": 1216,
|
||||
"sha256": "42c22c70bad16b01d8f70fa80977028b2789381772e4eff7bb231701e18293a1"
|
||||
},
|
||||
"fillEvents": {
|
||||
"blockCount": 61,
|
||||
"bytes": 7731746,
|
||||
"rowCount": 25408,
|
||||
"sha256": "c59a4a1e70195bd8a2421941e11b7fbb138d658796ef4d42466c44505b8353d8"
|
||||
},
|
||||
"holdingSnapshots": {
|
||||
"blockCount": 61,
|
||||
"bytes": 15176388,
|
||||
"rowCount": 33164,
|
||||
"sha256": "6e40d3f1d43005dc4f0c492662c4fdf600ca96e9abe874db4b56b9bd51e89cd4"
|
||||
},
|
||||
"orderEvents": {
|
||||
"blockCount": 61,
|
||||
"bytes": 5775840,
|
||||
"rowCount": 25541,
|
||||
"sha256": "14fa27db9328c4c6337157af4fc1efc1c4bc9f239a4605bf92439b0f3f426200"
|
||||
},
|
||||
"riskAudits": {
|
||||
"blockCount": 36,
|
||||
"bytes": 27622,
|
||||
"rowCount": 113,
|
||||
"sha256": "2f6d9a01ec568ed13943b9590e21d73bf614ce2c777302764533d2da0168b2d5"
|
||||
}
|
||||
},
|
||||
"sha256": "1eab5a5ffe59efeaba6b2048a33760aab31d42efa0fc10e35a4a7048db853235",
|
||||
"totalEvents": 111014
|
||||
},
|
||||
"schemaVersion": "fidc-backtest-artifacts/v4"
|
||||
}
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"resultConsistent": true,
|
||||
"resultStoreDigest": "1eab5a5ffe59efeaba6b2048a33760aab31d42efa0fc10e35a4a7048db853235",
|
||||
"canonicalResultDigest": "b29b085d43bcc0f8f1712767421781c70570a24112933623d4bbbef46508d710",
|
||||
"totalReturn": 0.9857987122635,
|
||||
"tradeCount": 25408,
|
||||
"terminalAudit": {
|
||||
"cashReceivableCount": 0,
|
||||
"cashReceivableTotalAmount": 0.0,
|
||||
"earliestDeferredCashDate": null,
|
||||
"futuresOpenOrderCount": 0,
|
||||
"lastExecutionDate": "2026-08-28",
|
||||
"omittedOpenOrderCount": 0,
|
||||
"openOrderSamples": [],
|
||||
"pendingCashFlowCount": 0,
|
||||
"pendingCashFlowNetAmount": 0.0,
|
||||
"status": "clean",
|
||||
"stockOpenOrderCount": 0
|
||||
},
|
||||
"terminalAuditSha256": "0e8503c2bfe9ecfd93bab3a95f79461830886eb693cfe6f477279908c0d95bd5",
|
||||
"implementation": {
|
||||
"buildManifestSha256": "fb4f7ac191c8ab8d8d39fc48014f84ce01d4c0731ffe986e7d5cb0107a3e5539",
|
||||
"builtAt": "2026-09-07T22:06:18+08:00",
|
||||
"engineCommit": "1b78186c4e37273c64c1f9e208c2e47021ee4fff",
|
||||
"identitySha256": "14e7ff10848d64ffcec232dea8cff23a032702301b81ae036bb0b26b4b0b5e33",
|
||||
"runnerBinarySha256": "7cddebf9ab5cbbd5d6f9bd5c7bf006c3895ee26967d23d80dc86b47c50fb761d",
|
||||
"schemaVersion": "fidc-backtest-implementation/v1",
|
||||
"serviceBinarySha256": "f09d121d73c34ad3cd5e2e0c24b0bdaf1974309c9ec3b93229cfb824b1e9f11a",
|
||||
"serviceCommit": "c80a38b91a398fd713f1f1faaa19a581ea85efad",
|
||||
"status": "verified"
|
||||
},
|
||||
"implementationIdentitySha256": "14e7ff10848d64ffcec232dea8cff23a032702301b81ae036bb0b26b4b0b5e33",
|
||||
"restartOrCold": {
|
||||
"queueWaitSeconds": 0.0,
|
||||
"executionSeconds": 21.61,
|
||||
"totalSeconds": 21.61,
|
||||
"runnerSeconds": 21.294,
|
||||
"bundleValidationSeconds": 0.004,
|
||||
"sourceEpochSeconds": 0.0,
|
||||
"strategyContractSeconds": 0.003,
|
||||
"terminalCalendarSeconds": 0.0,
|
||||
"riskFreeRateSeconds": 0.017,
|
||||
"riskFreeRateCacheHit": false,
|
||||
"engineSeconds": 5.566,
|
||||
"dataSeconds": 15.305,
|
||||
"resultSeconds": 0.395,
|
||||
"unattributedSeconds": 0.0,
|
||||
"canonicalSeconds": 0.206,
|
||||
"finalizationSeconds": 0.307
|
||||
},
|
||||
"processHotMedian": {
|
||||
"queueWaitSeconds": 0.0,
|
||||
"executionSeconds": 21.61,
|
||||
"totalSeconds": 21.61,
|
||||
"runnerSeconds": 21.294,
|
||||
"bundleValidationSeconds": 0.004,
|
||||
"sourceEpochSeconds": 0.0,
|
||||
"strategyContractSeconds": 0.003,
|
||||
"terminalCalendarSeconds": 0.0,
|
||||
"riskFreeRateSeconds": 0.017,
|
||||
"riskFreeRateCacheHit": false,
|
||||
"engineSeconds": 5.566,
|
||||
"dataSeconds": 15.305,
|
||||
"resultSeconds": 0.395,
|
||||
"unattributedSeconds": 0.0,
|
||||
"canonicalSeconds": 0.206,
|
||||
"finalizationSeconds": 0.307
|
||||
}
|
||||
}
|
||||
}
|
||||
+424
@@ -0,0 +1,424 @@
|
||||
{
|
||||
"strategy_id": "benchmark-native-factor-overlay",
|
||||
"strategy_version_id": "goal-five-year-semantics-v1",
|
||||
"user_id": "boris",
|
||||
"runtime": {
|
||||
"start_date": "2021-08-23",
|
||||
"end_date": "2026-08-28",
|
||||
"frequency": "1d",
|
||||
"source_table": "strategy_factory_source_lake.daily_source_rows_v1",
|
||||
"signal_symbol": "000852.SH",
|
||||
"benchmark_symbol": "000852.SH",
|
||||
"initial_cash": 10000000.0,
|
||||
"backtestDataBundleId": "bt_bundle_b44e03990c76064f54a9",
|
||||
"backtestDataBundleHash": "d7c1461131edaecb5981e207852782d92e636dbfee9fd7c44063605d96eb2b4f"
|
||||
},
|
||||
"execution": {
|
||||
"matchingType": "next_bar_open",
|
||||
"rebalanceCashMode": "same_point_net",
|
||||
"slippageModel": "price_ratio",
|
||||
"slippageValue": 0.0001,
|
||||
"commissionRate": 0.0001,
|
||||
"minimumCommission": 5.0,
|
||||
"stampTaxRateBeforeChange": 0.001,
|
||||
"stampTaxRateAfterChange": 0.0005,
|
||||
"stampTaxChangeDate": "2023-08-28",
|
||||
"volumeLimit": true,
|
||||
"liquidityLimit": false,
|
||||
"volumePercent": 0.25,
|
||||
"riskPolicy": {
|
||||
"allowMarketOrders": true,
|
||||
"blacklistEnabled": false,
|
||||
"blacklistedSymbols": [],
|
||||
"commissionRate": 0.0001,
|
||||
"forbidSameDayRebuyAfterSell": true,
|
||||
"liquidityLimitEnabled": false,
|
||||
"liveTradingEnabled": false,
|
||||
"minimumCommission": 5.0,
|
||||
"rejectBjseBuy": false,
|
||||
"rejectBjseSelection": false,
|
||||
"rejectInactiveBuy": true,
|
||||
"rejectInactiveSelection": false,
|
||||
"rejectInactiveSell": true,
|
||||
"rejectKcbBuy": true,
|
||||
"rejectKcbSelection": false,
|
||||
"rejectLowerLimitSelection": false,
|
||||
"rejectLowerLimitSell": true,
|
||||
"rejectNewListingBuy": true,
|
||||
"rejectNewListingSelection": false,
|
||||
"rejectOneYuanBuy": true,
|
||||
"rejectOneYuanSelection": false,
|
||||
"rejectPausedBuy": true,
|
||||
"rejectPausedSelection": false,
|
||||
"rejectPausedSell": true,
|
||||
"rejectStBuy": true,
|
||||
"rejectStSelection": false,
|
||||
"rejectStarStBuy": true,
|
||||
"rejectStarStSelection": false,
|
||||
"rejectUpperLimitBuy": true,
|
||||
"rejectUpperLimitSelection": false,
|
||||
"respectAllowBuySell": true,
|
||||
"stampTaxChangeDate": "2023-08-28",
|
||||
"stampTaxRateAfterChange": 0.0005,
|
||||
"stampTaxRateBeforeChange": 0.001,
|
||||
"volumeLimitEnabled": true,
|
||||
"volumePercent": 0.25
|
||||
}
|
||||
},
|
||||
"strategy_source": {
|
||||
"source_type": "platform-strategy",
|
||||
"language": "engine-script",
|
||||
"parser": "omniquant-engine-script-v2",
|
||||
"source_code": "strategy(\"xiaoshizhi_1_06_dynamic_small_cap_csi2000_signal_day_exposure\") {\n market(\"CN_A\");\n benchmark(\"000300.SH\");\n signal(\"932000.CSI\");\n\n let stocknum = 30;\n let candidate_pool_size = 50;\n let position_denominator_extra = 1;\n let signal_close_t = rolling_mean_current(\"signal_close\", 1);\n let signal_ma10_t = rolling_mean_current(\"signal_close\", 10);\n let signal_ma30_t = rolling_mean_current(\"signal_close\", 30);\n let signal_vol20_t = rolling_return_stddev_current(\"signal_close\", 20);\n let signal_high60_t = rolling_max_current(\"signal_close\", 60);\n let signal_drawdown60_t = 1.0 - safe_div(signal_close_t, signal_high60_t);\n let signal_range_t = safe_div(clamp(signal_close_t, 2000.0, 3000.0) - 2000.0, 1000.0);\n let market_cap_lower_t = 12.0 + signal_range_t * 5.0;\n let market_cap_upper_t = 40.0 + signal_range_t * 5.0;\n let base_exposure_t = signal_ma10_t > signal_ma30_t ? 1.0 : 0.3;\n let volatility_exposure_t = signal_vol20_t >= 0.025 ? 0.3 : 1.0;\n let drawdown_exposure_t = signal_drawdown60_t >= 0.08 ? 0.2 : 1.0;\n let final_exposure_t =\n signal_close_t > 0.0 &&\n signal_ma10_t > 0.0 &&\n signal_ma30_t > 0.0 &&\n signal_high60_t > 0.0\n ? min(min(base_exposure_t, volatility_exposure_t), drawdown_exposure_t)\n : 0.0;\n\n rebalance.every_days(1).at([\"15:00\"]);\n\n selection.market_cap_band(\n field=\"market_cap\",\n lower=market_cap_lower_t,\n upper=market_cap_upper_t\n );\n\n filter.stock_expr(((!is_star_st && !is_kcb && !is_bjse && rolling_mean_current(\"close\", 5) > rolling_mean_current(\"close\", 10) && rolling_mean_current(\"close\", 10) > rolling_mean_current(\"close\", 30) && rolling_mean_current(\"volume\", 5) < rolling_mean_current(\"volume\", 100)) && (!is_st))) && (ths_up_days_stock >= 1);\n\n ordering.rank_by(\"market_cap\", \"asc\");\n selection.candidate_limit(50);\n selection.limit(stocknum);\n\n allocation.buy_scale(30.0 / 31.0);\n execution.strict_value_budget(true)\n\n trading.hold_until_exit(true);\n trading.max_holding_days(90);\n trading.daily_top_up(true);\n trading.daily_position_target_adjust(true);\n trading.target_portfolio_daily(true);\n trading.rebalance_existing_positions(true);\n trading.retry_empty_rebalance(true);\n trading.release_slot_on_exit_signal(true);\n\n risk.stop_loss(0.08);\n risk.take_profit(0.16);\n risk.reference_price_mode(\"signal_day_post_adjusted_close\");\n risk.index_exposure(final_exposure_t);\n\n risk.policy(reject_st_selection=false, reject_st_buy=true, reject_star_st_selection=false, reject_star_st_buy=true, reject_paused_selection=false, reject_paused_buy=true, reject_paused_sell=true, reject_inactive_selection=false, reject_inactive_buy=true, reject_inactive_sell=true, reject_new_listing_selection=false, reject_new_listing_buy=true, reject_kcb_selection=false, reject_kcb_buy=true, reject_bjse_selection=false, reject_bjse_buy=false, reject_one_yuan_selection=false, reject_one_yuan_buy=true, respect_allow_buy_sell=true, reject_upper_limit_selection=false, reject_lower_limit_selection=false, reject_upper_limit_buy=true, reject_lower_limit_sell=true, forbid_same_day_rebuy_after_sell=true, blacklist_enabled=false, blacklisted_symbols=[], allow_market_orders=true, live_trading_enabled=false, volume_limit_enabled=true, liquidity_limit_enabled=false, volume_percent=0.25, commission_rate=0.0001, minimum_commission=5.0, stamp_tax_rate_before_change=0.001, stamp_tax_rate_after_change=0.0005, stamp_tax_change_date=\"2023-08-28\");\n\n execution.matching_type(\"next_bar_open\");\n execution.slippage(\"price_ratio\", 0.0001);\n execution.rebalance_cash_mode(\"same_point_net\");\n}"
|
||||
},
|
||||
"strategy_spec": {
|
||||
"benchmark": {
|
||||
"fallbackInstrumentId": "000852.SH",
|
||||
"instrumentId": "000852.SH"
|
||||
},
|
||||
"engineConfig": {
|
||||
"benchmarkSymbol": "000852.SH",
|
||||
"commissionRate": 0.0001,
|
||||
"dividendReinvestment": false,
|
||||
"dynamicRange": {
|
||||
"baseCapFloor": 7,
|
||||
"baseIndexLevel": 2000,
|
||||
"capSpan": 10,
|
||||
"xs": 0.008
|
||||
},
|
||||
"frequency": "1d",
|
||||
"indexThrottle": {
|
||||
"defensiveExposure": 0.5,
|
||||
"fullExposure": 1,
|
||||
"longDays": 130,
|
||||
"rsiRate": 1.0001,
|
||||
"shortDays": 1
|
||||
},
|
||||
"liquidityLimit": false,
|
||||
"matchingType": "next_bar_open",
|
||||
"minimumCommission": 5.0,
|
||||
"rankLimit": 30,
|
||||
"rebalanceCashMode": "same_point_net",
|
||||
"rebalanceSchedule": {
|
||||
"frequency": "daily",
|
||||
"time": "15:00"
|
||||
},
|
||||
"refreshRate": 1,
|
||||
"riskPolicy": {
|
||||
"allowMarketOrders": true,
|
||||
"blacklistEnabled": false,
|
||||
"blacklistedSymbols": [],
|
||||
"commissionRate": 0.0001,
|
||||
"forbidSameDayRebuyAfterSell": true,
|
||||
"liquidityLimitEnabled": false,
|
||||
"liveTradingEnabled": false,
|
||||
"minimumCommission": 5.0,
|
||||
"rejectBjseBuy": false,
|
||||
"rejectBjseSelection": false,
|
||||
"rejectInactiveBuy": true,
|
||||
"rejectInactiveSelection": false,
|
||||
"rejectInactiveSell": true,
|
||||
"rejectKcbBuy": true,
|
||||
"rejectKcbSelection": false,
|
||||
"rejectLowerLimitSelection": false,
|
||||
"rejectLowerLimitSell": true,
|
||||
"rejectNewListingBuy": true,
|
||||
"rejectNewListingSelection": false,
|
||||
"rejectOneYuanBuy": true,
|
||||
"rejectOneYuanSelection": false,
|
||||
"rejectPausedBuy": true,
|
||||
"rejectPausedSelection": false,
|
||||
"rejectPausedSell": true,
|
||||
"rejectStBuy": true,
|
||||
"rejectStSelection": false,
|
||||
"rejectStarStBuy": true,
|
||||
"rejectStarStSelection": false,
|
||||
"rejectUpperLimitBuy": true,
|
||||
"rejectUpperLimitSelection": false,
|
||||
"respectAllowBuySell": true,
|
||||
"stampTaxChangeDate": "2023-08-28",
|
||||
"stampTaxRateAfterChange": 0.0005,
|
||||
"stampTaxRateBeforeChange": 0.001,
|
||||
"volumeLimitEnabled": true,
|
||||
"volumePercent": 0.25
|
||||
},
|
||||
"rsiRate": 1.0001,
|
||||
"signalSymbol": "000852.SH",
|
||||
"skipWindows": [],
|
||||
"slippageModel": "price_ratio",
|
||||
"slippageValue": 0.0001,
|
||||
"stampTaxChangeDate": "2023-08-28",
|
||||
"stampTaxRateAfterChange": 0.0005,
|
||||
"stampTaxRateBeforeChange": 0.001,
|
||||
"stockMaFilter": {
|
||||
"longDays": 30,
|
||||
"midDays": 10,
|
||||
"rsiRate": 1.0001,
|
||||
"shortDays": 5,
|
||||
"volumeLongDays": 100,
|
||||
"volumeShortDays": 5
|
||||
},
|
||||
"stopLossMultiplier": 0.08,
|
||||
"strictValueBudget": true,
|
||||
"takeProfitMultiplier": 0.16,
|
||||
"templateId": "xiaoshizhi_1_06_dynamic_small_cap_csi2000_signal_day_exposure",
|
||||
"volumeLimit": true,
|
||||
"volumePercent": 0.25
|
||||
},
|
||||
"execution": {
|
||||
"commissionRate": 0.0001,
|
||||
"executionGranularity": "daily_or_minute_bar",
|
||||
"extractor": "omniquant-engine-script-v2",
|
||||
"frequency": "1d",
|
||||
"liquidityLimit": false,
|
||||
"matchingType": "next_bar_open",
|
||||
"minimumCommission": 5.0,
|
||||
"priceSource": "current_bar_close_or_next_bar_open_or_minute_bar",
|
||||
"rebalanceCashMode": "same_point_net",
|
||||
"riskPolicy": {
|
||||
"allowMarketOrders": true,
|
||||
"blacklistEnabled": false,
|
||||
"blacklistedSymbols": [],
|
||||
"commissionRate": 0.0001,
|
||||
"forbidSameDayRebuyAfterSell": true,
|
||||
"liquidityLimitEnabled": false,
|
||||
"liveTradingEnabled": false,
|
||||
"minimumCommission": 5.0,
|
||||
"rejectBjseBuy": false,
|
||||
"rejectBjseSelection": false,
|
||||
"rejectInactiveBuy": true,
|
||||
"rejectInactiveSelection": false,
|
||||
"rejectInactiveSell": true,
|
||||
"rejectKcbBuy": true,
|
||||
"rejectKcbSelection": false,
|
||||
"rejectLowerLimitSelection": false,
|
||||
"rejectLowerLimitSell": true,
|
||||
"rejectNewListingBuy": true,
|
||||
"rejectNewListingSelection": false,
|
||||
"rejectOneYuanBuy": true,
|
||||
"rejectOneYuanSelection": false,
|
||||
"rejectPausedBuy": true,
|
||||
"rejectPausedSelection": false,
|
||||
"rejectPausedSell": true,
|
||||
"rejectStBuy": true,
|
||||
"rejectStSelection": false,
|
||||
"rejectStarStBuy": true,
|
||||
"rejectStarStSelection": false,
|
||||
"rejectUpperLimitBuy": true,
|
||||
"rejectUpperLimitSelection": false,
|
||||
"respectAllowBuySell": true,
|
||||
"stampTaxChangeDate": "2023-08-28",
|
||||
"stampTaxRateAfterChange": 0.0005,
|
||||
"stampTaxRateBeforeChange": 0.001,
|
||||
"volumeLimitEnabled": true,
|
||||
"volumePercent": 0.25
|
||||
},
|
||||
"selectionGranularity": "strategy_factory_source_lake.daily_source_rows_v1",
|
||||
"slippageModel": "price_ratio",
|
||||
"slippageValue": 0.0001,
|
||||
"sourceKind": "platform-strategy",
|
||||
"sourceLanguage": "engine-script",
|
||||
"stampTaxChangeDate": "2023-08-28",
|
||||
"stampTaxRateAfterChange": 0.0005,
|
||||
"stampTaxRateBeforeChange": 0.001,
|
||||
"strictValueBudget": true,
|
||||
"volumeLimit": true,
|
||||
"volumePercent": 0.25
|
||||
},
|
||||
"factorRefs": [
|
||||
"market_cap",
|
||||
"ths_up_days_stock"
|
||||
],
|
||||
"market": "CN_A",
|
||||
"metadata": {
|
||||
"backtestDataBundle": {
|
||||
"sourceTable": "strategy_factory_source_lake.daily_source_rows_v1",
|
||||
"backtestDataBundleId": "bt_bundle_b44e03990c76064f54a9",
|
||||
"backtestDataBundleHash": "d7c1461131edaecb5981e207852782d92e636dbfee9fd7c44063605d96eb2b4f"
|
||||
},
|
||||
"backtestDataBundleHash": "d7c1461131edaecb5981e207852782d92e636dbfee9fd7c44063605d96eb2b4f",
|
||||
"backtestDataBundleId": "bt_bundle_b44e03990c76064f54a9",
|
||||
"sourceTable": "strategy_factory_source_lake.daily_source_rows_v1"
|
||||
},
|
||||
"rebalance": {
|
||||
"dailyApproximation": "日线回测按 matching_type 撮合;分钟线回测按交易时刻分钟价格撮合",
|
||||
"frequencyDays": 1,
|
||||
"schedule": {
|
||||
"frequency": "daily",
|
||||
"time": "15:00"
|
||||
},
|
||||
"tradeTimes": [
|
||||
"15:00"
|
||||
]
|
||||
},
|
||||
"risk": {
|
||||
"indexThrottleExpr": "final_exposure_t",
|
||||
"stopLossExpr": "0.08",
|
||||
"stopTakeReferencePriceMode": "signal_day_post_adjusted_close",
|
||||
"takeProfitExpr": "0.16"
|
||||
},
|
||||
"runtimeExpressions": {
|
||||
"allocation": {
|
||||
"buyScaleExpr": "30.0 / 31.0"
|
||||
},
|
||||
"ordering": {
|
||||
"rankBy": "market_cap",
|
||||
"rankExpr": "",
|
||||
"rankOrder": "asc"
|
||||
},
|
||||
"prelude": "let stocknum = 30;\nlet candidate_pool_size = 50;\nlet position_denominator_extra = 1;\nlet signal_close_t = rolling_mean_current(\"signal_close\", 1);\nlet signal_ma10_t = rolling_mean_current(\"signal_close\", 10);\nlet signal_ma30_t = rolling_mean_current(\"signal_close\", 30);\nlet signal_vol20_t = rolling_return_stddev_current(\"signal_close\", 20);\nlet signal_high60_t = rolling_max_current(\"signal_close\", 60);\nlet signal_drawdown60_t = 1.0 - safe_div(signal_close_t, signal_high60_t);\nlet signal_range_t = safe_div(clamp(signal_close_t, 2000.0, 3000.0) - 2000.0, 1000.0);\nlet market_cap_lower_t = 12.0 + signal_range_t * 5.0;\nlet market_cap_upper_t = 40.0 + signal_range_t * 5.0;\nlet base_exposure_t = signal_ma10_t > signal_ma30_t ? 1.0 : 0.3;\nlet volatility_exposure_t = signal_vol20_t >= 0.025 ? 0.3 : 1.0;\nlet drawdown_exposure_t = signal_drawdown60_t >= 0.08 ? 0.2 : 1.0;\nlet final_exposure_t = signal_close_t > 0.0 && signal_ma10_t > 0.0 && signal_ma30_t > 0.0 && signal_high60_t > 0.0 ? min(min(base_exposure_t, volatility_exposure_t), drawdown_exposure_t) : 0.0;\nlet warmup_probe = rolling_sum(\"amount\", 125);",
|
||||
"risk": {
|
||||
"exposureExpr": "final_exposure_t",
|
||||
"stopLossExpr": "0.08",
|
||||
"stopTakeReferencePriceMode": "signal_day_post_adjusted_close",
|
||||
"takeProfitExpr": "0.16"
|
||||
},
|
||||
"schedule": {
|
||||
"frequency": "daily",
|
||||
"time": "15:00"
|
||||
},
|
||||
"selection": {
|
||||
"candidateLimitExpr": "50",
|
||||
"limitExpr": "stocknum",
|
||||
"marketCapField": "market_cap",
|
||||
"marketCapLowerExpr": "market_cap_lower_t",
|
||||
"marketCapUpperExpr": "market_cap_upper_t",
|
||||
"stockFilterExpr": "(((!is_star_st && !is_kcb && !is_bjse && rolling_mean_current(\"close\", 5) > rolling_mean_current(\"close\", 10) && rolling_mean_current(\"close\", 10) > rolling_mean_current(\"close\", 30) && rolling_mean_current(\"volume\", 5) < rolling_mean_current(\"volume\", 100)) && (!is_st))) && (ths_up_days_stock >= 1)"
|
||||
},
|
||||
"trading": {
|
||||
"actions": [],
|
||||
"dailyPositionTargetAdjust": true,
|
||||
"dailyTopUp": true,
|
||||
"holdUntilExit": true,
|
||||
"maxHoldingDays": 90,
|
||||
"rebalanceExistingPositions": true,
|
||||
"releaseSlotOnExitSignal": true,
|
||||
"retryEmptyRebalance": true,
|
||||
"rotationEnabled": true,
|
||||
"stage": "on_day",
|
||||
"subscriptionGuardRequired": false,
|
||||
"targetPortfolioDaily": true
|
||||
}
|
||||
},
|
||||
"seasonality": {
|
||||
"skipWindows": []
|
||||
},
|
||||
"selectors": [
|
||||
{
|
||||
"field": "market_cap",
|
||||
"lowerExpr": "market_cap_lower_t",
|
||||
"mapping": "market_cap -> strategy_factory_source_lake.runtime_fields.market_cap",
|
||||
"type": "dynamicRange",
|
||||
"upperExpr": "market_cap_upper_t"
|
||||
},
|
||||
{
|
||||
"expr": "(((!is_star_st && !is_kcb && !is_bjse && rolling_mean_current(\"close\", 5) > rolling_mean_current(\"close\", 10) && rolling_mean_current(\"close\", 10) > rolling_mean_current(\"close\", 30) && rolling_mean_current(\"volume\", 5) < rolling_mean_current(\"volume\", 100)) && (!is_st))) && (ths_up_days_stock >= 1)",
|
||||
"type": "filter"
|
||||
},
|
||||
{
|
||||
"limitExpr": "stocknum",
|
||||
"orderBy": [
|
||||
"market_cap asc"
|
||||
],
|
||||
"type": "rank"
|
||||
}
|
||||
],
|
||||
"signalSymbol": "000852.SH",
|
||||
"sourceCode": "strategy(\"xiaoshizhi_1_06_dynamic_small_cap_csi2000_signal_day_exposure\") {\n market(\"CN_A\");\n benchmark(\"000300.SH\");\n signal(\"932000.CSI\");\n\n let stocknum = 30;\n let candidate_pool_size = 50;\n let position_denominator_extra = 1;\n let signal_close_t = rolling_mean_current(\"signal_close\", 1);\n let signal_ma10_t = rolling_mean_current(\"signal_close\", 10);\n let signal_ma30_t = rolling_mean_current(\"signal_close\", 30);\n let signal_vol20_t = rolling_return_stddev_current(\"signal_close\", 20);\n let signal_high60_t = rolling_max_current(\"signal_close\", 60);\n let signal_drawdown60_t = 1.0 - safe_div(signal_close_t, signal_high60_t);\n let signal_range_t = safe_div(clamp(signal_close_t, 2000.0, 3000.0) - 2000.0, 1000.0);\n let market_cap_lower_t = 12.0 + signal_range_t * 5.0;\n let market_cap_upper_t = 40.0 + signal_range_t * 5.0;\n let base_exposure_t = signal_ma10_t > signal_ma30_t ? 1.0 : 0.3;\n let volatility_exposure_t = signal_vol20_t >= 0.025 ? 0.3 : 1.0;\n let drawdown_exposure_t = signal_drawdown60_t >= 0.08 ? 0.2 : 1.0;\n let final_exposure_t =\n signal_close_t > 0.0 &&\n signal_ma10_t > 0.0 &&\n signal_ma30_t > 0.0 &&\n signal_high60_t > 0.0\n ? min(min(base_exposure_t, volatility_exposure_t), drawdown_exposure_t)\n : 0.0;\n\n rebalance.every_days(1).at([\"15:00\"]);\n\n selection.market_cap_band(\n field=\"market_cap\",\n lower=market_cap_lower_t,\n upper=market_cap_upper_t\n );\n\n filter.stock_expr(((!is_star_st && !is_kcb && !is_bjse && rolling_mean_current(\"close\", 5) > rolling_mean_current(\"close\", 10) && rolling_mean_current(\"close\", 10) > rolling_mean_current(\"close\", 30) && rolling_mean_current(\"volume\", 5) < rolling_mean_current(\"volume\", 100)) && (!is_st))) && (ths_up_days_stock >= 1);\n\n ordering.rank_by(\"market_cap\", \"asc\");\n selection.candidate_limit(50);\n selection.limit(stocknum);\n\n allocation.buy_scale(30.0 / 31.0);\n execution.strict_value_budget(true)\n\n trading.hold_until_exit(true);\n trading.max_holding_days(90);\n trading.daily_top_up(true);\n trading.daily_position_target_adjust(true);\n trading.target_portfolio_daily(true);\n trading.rebalance_existing_positions(true);\n trading.retry_empty_rebalance(true);\n trading.release_slot_on_exit_signal(true);\n\n risk.stop_loss(0.08);\n risk.take_profit(0.16);\n risk.reference_price_mode(\"signal_day_post_adjusted_close\");\n risk.index_exposure(final_exposure_t);\n\n risk.policy(reject_st_selection=false, reject_st_buy=true, reject_star_st_selection=false, reject_star_st_buy=true, reject_paused_selection=false, reject_paused_buy=true, reject_paused_sell=true, reject_inactive_selection=false, reject_inactive_buy=true, reject_inactive_sell=true, reject_new_listing_selection=false, reject_new_listing_buy=true, reject_kcb_selection=false, reject_kcb_buy=true, reject_bjse_selection=false, reject_bjse_buy=false, reject_one_yuan_selection=false, reject_one_yuan_buy=true, respect_allow_buy_sell=true, reject_upper_limit_selection=false, reject_lower_limit_selection=false, reject_upper_limit_buy=true, reject_lower_limit_sell=true, forbid_same_day_rebuy_after_sell=true, blacklist_enabled=false, blacklisted_symbols=[], allow_market_orders=true, live_trading_enabled=false, volume_limit_enabled=true, liquidity_limit_enabled=false, volume_percent=0.25, commission_rate=0.0001, minimum_commission=5.0, stamp_tax_rate_before_change=0.001, stamp_tax_rate_after_change=0.0005, stamp_tax_change_date=\"2023-08-28\");\n\n execution.matching_type(\"next_bar_open\");\n execution.slippage(\"price_ratio\", 0.0001);\n execution.rebalance_cash_mode(\"same_point_net\");\n}",
|
||||
"strategyId": "warmup-expression-contract-acceptance",
|
||||
"universe": {
|
||||
"exclude": [],
|
||||
"implementationNotes": [
|
||||
"ST、停牌、退市、新股、科创、一元、涨跌停、同日卖出禁买、成交量和费用由 riskPolicy / RiskLimits 统一执行",
|
||||
"上市日期与退市日期取自 instrument 结构化字段,不再使用股票名称做 ST/退市判断",
|
||||
"盘中 current_price / last_price 由策略交易时刻批量 tick 查询驱动"
|
||||
]
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"stockPoolFactorContract": {
|
||||
"schemaVersion": 1,
|
||||
"entryLogic": "all",
|
||||
"exitLogic": "any",
|
||||
"conditions": [
|
||||
{
|
||||
"factorRef": "up_days_stock",
|
||||
"label": "连涨天数",
|
||||
"role": "selection",
|
||||
"registryRole": "selection_feature",
|
||||
"roleRegistrySha256": "1d0b307c168feda08d5fbe20f0e88964230553f8ceb2017b66aec48dbd5a5b57",
|
||||
"roleEvidence": {
|
||||
"role": "selection_feature",
|
||||
"polarity": "trend_persistence_positive",
|
||||
"signalShape": "state",
|
||||
"holdingStates": [
|
||||
"flat"
|
||||
],
|
||||
"requiredConfirmations": [],
|
||||
"cooldownTradingDays": 0,
|
||||
"windowTradingDays": 1,
|
||||
"recommendedParameters": {
|
||||
"inputUnit": "days",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"operator": ">=",
|
||||
"threshold": 1,
|
||||
"semantic": {
|
||||
"ref": "up_days_stock",
|
||||
"label": "连涨天数",
|
||||
"status": "available",
|
||||
"queryable": true,
|
||||
"source": "strategy-factory-source-lake:indicator",
|
||||
"schema": "strategy-factory.value-semantics/v1",
|
||||
"valueType": "integer",
|
||||
"semanticType": "count",
|
||||
"comparisonGroup": "count",
|
||||
"storageUnit": "days",
|
||||
"inputUnit": "days",
|
||||
"inputScale": 1.0,
|
||||
"allowedOperators": [
|
||||
">",
|
||||
">=",
|
||||
"<",
|
||||
"<=",
|
||||
"==",
|
||||
"!=",
|
||||
"between",
|
||||
"in"
|
||||
],
|
||||
"nullable": true,
|
||||
"declared": true,
|
||||
"metadataStatus": "declared",
|
||||
"semanticProvenance": "explicit_manifest",
|
||||
"businessSemanticDeclared": true,
|
||||
"minimum": 0,
|
||||
"backtestBinding": {
|
||||
"field": "ths_up_days_stock",
|
||||
"sourceDataset": "indicators_up_days_stock"
|
||||
},
|
||||
"tradingRoles": [
|
||||
{
|
||||
"role": "selection_feature",
|
||||
"polarity": "trend_persistence_positive",
|
||||
"signalShape": "state",
|
||||
"holdingStates": [
|
||||
"flat"
|
||||
],
|
||||
"requiredConfirmations": [],
|
||||
"cooldownTradingDays": 0,
|
||||
"windowTradingDays": 1,
|
||||
"recommendedParameters": {
|
||||
"inputUnit": "days",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"tradingRoleTradable": true,
|
||||
"tradingRoleEvidenceStatus": "source_lake_registered_indicator",
|
||||
"tradingRoleRegistrySha256": "1d0b307c168feda08d5fbe20f0e88964230553f8ceb2017b66aec48dbd5a5b57"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"date": "2026-09-07",
|
||||
"host": "192.168.31.177",
|
||||
"identity": "boris",
|
||||
"implementationCommit": "a02ac6e",
|
||||
"valueRegressionCommit": "cb97aa1",
|
||||
"scope": "Native daily indicator fields explicitly bound in stockPoolFactorContract; other factor fields and pricing are unchanged.",
|
||||
"targetedTests": {"passed": 3, "failed": 0},
|
||||
"fullLibraryTestsBeforeAdditionalValueCase": {"passed": 447, "ignored": 6, "failed": 0},
|
||||
"provenCases": [
|
||||
"09:30, 10:30 and 14:30 resolve to the preceding trading date",
|
||||
"15:00 resolves to the completed decision day",
|
||||
"active intraday datetime applies when no explicit execution time exists",
|
||||
"next-open retains the completed decision day",
|
||||
"no previous trading date does not fall back to the current day",
|
||||
"stock state with prior value 2 and current value 999 reads 2 intraday and 999 at close",
|
||||
"unbound factor value remains unchanged"
|
||||
],
|
||||
"backtestServiceDeployed": true,
|
||||
"paperLiveRuntimeDeployed": true,
|
||||
"paperLiveDeploymentEvidence": "/Users/boris/WorkSpace/fidc-trading-platform/docs/evidence/trading-engine-revision-deployment-20260907.json",
|
||||
"realBacktestAcceptanceComplete": false,
|
||||
"scopedBacktestEvidence": {
|
||||
"intraday": {
|
||||
"range": "2025-09-08..2025-09-12",
|
||||
"time": "09:30",
|
||||
"runIds": ["btr_1788790021780_1150210_0", "btr_1788790036494_1150210_1"],
|
||||
"seconds": [8.994, 0.596],
|
||||
"tradeCount": 104,
|
||||
"riskDecisionCount": 11,
|
||||
"canonical": "5c8a110cc6f285b9d569e818a0472a8b5c76f14df853a1c2c42d1b5222c39b3a",
|
||||
"identical": true,
|
||||
"persistedFactorBindingVerified": true,
|
||||
"rawParquetAudit": {
|
||||
"buyFills": 60,
|
||||
"priorPassCurrentFailExamples": 21,
|
||||
"existingPositionTopUpsBelowCurrentSelectionThreshold": 23,
|
||||
"retainedTargetReentryBelowCurrentSelectionThreshold": {"symbol": "600276.SH", "date": "2025-09-12", "priorExit": "2025-09-11 stop_loss_exit", "configuration": "reenterExitedTargets=true", "reason": "model_target_portfolio_daily"},
|
||||
"note": "Selection-only conditions are not an execution-time buy veto. Position adjustment and explicit retained-target reentry must be audited separately from fresh candidate selection."
|
||||
}
|
||||
},
|
||||
"nextOpen": {
|
||||
"range": "2021-08-23..2026-08-28",
|
||||
"runId": "btr_1788790344805_1150210_2",
|
||||
"seconds": 21.610,
|
||||
"tradeCount": 25408,
|
||||
"canonical": "b29b085d43bcc0f8f1712767421781c70570a24112933623d4bbbef46508d710",
|
||||
"matchesPreFixBaseline": true
|
||||
},
|
||||
"terminalAudits": "clean",
|
||||
"rawEvidenceDirectory": "native-daily-factor-replays-20260907"
|
||||
},
|
||||
"limitations": [
|
||||
"This is not a generic per-field publication-timestamp model for all factor datasets.",
|
||||
"Raw dynamic fields used without a stock-pool native binding need separate availability-contract review.",
|
||||
"Broader factor/PIT and actual trading acceptance remain required; these replays use isolated API research fixtures. Browser draft handoff is separately recorded in OmniQuant documentation."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"schemaVersion": "fidc-noalloc-instrument-board-rules/v1",
|
||||
"measuredAt": "2026-09-05T03:48:00+08:00",
|
||||
"host": "192.168.31.177",
|
||||
"engineCommit": "cfb19b5783cb446099cb3e4aff70cc39beec2e88",
|
||||
"serviceCommit": "9fd5a9e6d5668af57f6942fc3c4127953545d9c6",
|
||||
"runnerBinarySha256": "3e69af42e41321d31c69b552cf22d7033ce1ea8d94305a32e32461148cdbfc60",
|
||||
"implementation": {
|
||||
"description": "evaluate KSH and BJSE order quantity rules with borrowed case-insensitive comparisons instead of allocating normalized board strings",
|
||||
"rulesChanged": false,
|
||||
"cacheAdded": false,
|
||||
"pitSemanticsChanged": false,
|
||||
"coveredBoards": ["KSH", "BJS", "BJ", "BJSE", "default"]
|
||||
},
|
||||
"primaryFiveYearContract": {
|
||||
"totalReturn": 0.9219861819172002,
|
||||
"tradeCount": 26088,
|
||||
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
|
||||
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
|
||||
"processCold": {
|
||||
"totalSeconds": 17.395,
|
||||
"dataSeconds": 12.879,
|
||||
"engineSeconds": 3.873
|
||||
},
|
||||
"processHotEngineSeconds": [3.861, 3.812, 3.896, 3.978, 3.924],
|
||||
"processHotMedianEngineSeconds": 3.896,
|
||||
"snapshotSourceBaselineMedianEngineSeconds": 3.91,
|
||||
"observedMedianImprovementPercent": 0.3580562659846607,
|
||||
"resultConsistent": true,
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"secondaryFiveYearContract": {
|
||||
"totalReturn": 1.1342962298106998,
|
||||
"tradeCount": 19404,
|
||||
"canonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
|
||||
"resultStoreSha256": "416d2f87241fb4c6b917f6aeecb588f82f6e7d51a103f4c53a74d11971f16839",
|
||||
"engineSeconds": [3.519, 3.501, 3.582, 3.597],
|
||||
"medianEngineSeconds": 3.582,
|
||||
"snapshotSourceBaselineMedianEngineSeconds": 3.676,
|
||||
"observedMedianImprovementPercent": 2.5571273122959823,
|
||||
"resultConsistent": true,
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"profile": {
|
||||
"runId": "btr_1788551290580_3858934_10",
|
||||
"engineSeconds": 4.283,
|
||||
"minimumOrderQuantityPercent": 0.21,
|
||||
"orderStepSizePercent": 0.11,
|
||||
"snapshotSourceBaselineMinimumOrderQuantityPercent": 1.28,
|
||||
"minimumOrderQuantityRelativeReductionPercent": 83.59375
|
||||
},
|
||||
"memory": {
|
||||
"serviceCgroupCurrentBytes": 11477078016,
|
||||
"serviceCgroupPeakBytes": 11478847488,
|
||||
"processRssKiB": 11195964,
|
||||
"processAnonymousKiB": 11179928,
|
||||
"cacheMemoryAddedBytes": 0
|
||||
},
|
||||
"testGate": {
|
||||
"coreUnitTotal": 422,
|
||||
"corePassed": 416,
|
||||
"ignoredManualBenchmarks": 6,
|
||||
"failed": 0,
|
||||
"integrationSuitesPassed": true
|
||||
},
|
||||
"remoteArtifacts": [
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/noalloc-instrument-board-primary-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/noalloc-instrument-board-secondary-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/noalloc-instrument-board-profile-20260905/perf.data",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/noalloc-instrument-board-profile-20260905/perf-report.txt"
|
||||
],
|
||||
"acceptance": {
|
||||
"status": "accepted_generic_no_allocation_board_rules",
|
||||
"reason": "both five-year contracts preserve exact outputs, the primary does not regress, the secondary improves, the targeted profile hotspot falls, and no cache memory is added"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"schemaVersion": "fidc-numeric-vm-binding-generation-rejection/v1",
|
||||
"measuredAt": "2026-09-06T05:36:00+08:00",
|
||||
"host": "192.168.31.177",
|
||||
"baseline": {
|
||||
"engineCommit": "5f08978",
|
||||
"primaryFiveYearHotMedianEngineSeconds": 2.629,
|
||||
"primaryFiveYearTotalReturn": 0.9219861819172002,
|
||||
"primaryFiveYearTradeCount": 26088,
|
||||
"primaryCanonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
|
||||
"primaryResultStoreSha256": "9ccf0c0fc6f5d72e974381ad4cd09a80241d7de99f2649f2b01736f7c80dc2c7",
|
||||
"profileInstructions": 16600366059,
|
||||
"profileBranches": 2592214949
|
||||
},
|
||||
"compileTimeIdentifierBinding": {
|
||||
"engineCommit": "2135a5b",
|
||||
"implementation": "map every numeric VM identifier to a typed runtime enum during expression plan compilation",
|
||||
"primaryFiveYearHotEngineSeconds": [2.608, 2.624, 2.651, 2.619, 2.781],
|
||||
"primaryFiveYearHotMedianEngineSeconds": 2.624,
|
||||
"observedMedianImprovementPercent": 0.190186,
|
||||
"profileInstructions": 16360894880,
|
||||
"instructionReductionPercent": 1.442566,
|
||||
"secondaryFiveYearResultConsistent": true,
|
||||
"secondaryFiveYearPerformanceExcluded": true,
|
||||
"secondaryFiveYearExclusionReason": "the symbolic campaign entered a high-memory-bandwidth phase between the primary and secondary batches",
|
||||
"netCodeLinesAdded": 562,
|
||||
"retained": false
|
||||
},
|
||||
"generationStampedScratchSlots": {
|
||||
"engineCommit": "5122c73",
|
||||
"implementation": "invalidate numeric VM variable and local slots with a generation counter instead of clearing Option arrays for each evaluation",
|
||||
"localReleaseBenchmark": {
|
||||
"baselineVmNanosecondsPerEvaluation": 86.992,
|
||||
"candidateSamples": [83.284, 94.166, 86.275, 84.138, 85.453],
|
||||
"candidateMedianNanosecondsPerEvaluation": 85.453,
|
||||
"componentImprovementPercent": 1.769136,
|
||||
"comparisonStrength": "weak because the baseline contains one sample"
|
||||
},
|
||||
"primaryFiveYearHotEngineSeconds": [2.634, 2.636, 2.608, 2.642, 2.710],
|
||||
"primaryFiveYearHotMedianEngineSeconds": 2.636,
|
||||
"observedMedianRegressionPercent": 0.266261,
|
||||
"profileInstructions": 16521146700,
|
||||
"instructionReductionPercent": 0.477215,
|
||||
"netCodeLinesAdded": 39,
|
||||
"retained": false
|
||||
},
|
||||
"hostLoad": {
|
||||
"symbolicWorkersObserved": 3,
|
||||
"symbolicWorkerCpuPercentApproximate": [720, 718, 698],
|
||||
"symbolicWorkerRssBytesApproximate": [54479982592, 53353455616, 53941170176],
|
||||
"wallTimeComparisonsAcrossPhasesExcluded": true
|
||||
},
|
||||
"testGate": {
|
||||
"workspacePassedBeforeFirstCandidateRejection": 539,
|
||||
"workspacePassedForGenerationCandidate": 540,
|
||||
"failed": 0,
|
||||
"ignoredManualBenchmarks": 8
|
||||
},
|
||||
"remoteArtifacts": [
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/goal-primary-five-year-20260906.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/identifier-binding-primary-five-year-20260906.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/identifier-binding-secondary-five-year-20260906.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/identifier-binding-candidate-perf-stat-20260906.csv",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/identifier-binding-rollback-primary-five-year-20260906.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/identifier-binding-rollback-perf-stat-20260906.csv",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/vm-generation-primary-five-year-20260906.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/vm-generation-candidate-perf-stat-20260906.csv"
|
||||
],
|
||||
"decision": {
|
||||
"status": "rejected_and_removed",
|
||||
"reason": "both candidates preserved exact business results but failed to produce a material, stable end-to-end improvement; the typed binding added disproportionate code and the generation slots slightly regressed the five-year median",
|
||||
"nextTarget": "profile and specialize the numeric VM instruction dispatch or runtime helper execution without changing expression, PIT, or lazy short-circuit semantics"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"schemaVersion": "fidc-numeric-vm-generation-scratch-rejection/v1",
|
||||
"measuredAt": "2026-09-05T04:34:00+08:00",
|
||||
"host": "local-macos",
|
||||
"candidateCommitted": false,
|
||||
"candidateDeployed": false,
|
||||
"candidate": {
|
||||
"description": "replace per-evaluation Option slot clearing with value arrays and u64 generation stamps",
|
||||
"iterations": 2000000,
|
||||
"vmNanosecondsPerEvaluation": 86.405,
|
||||
"rhaiNanosecondsPerEvaluation": 364.356
|
||||
},
|
||||
"baseline": {
|
||||
"iterations": 2000000,
|
||||
"vmNanosecondsPerEvaluation": 86.384,
|
||||
"rhaiNanosecondsPerEvaluation": 374.938
|
||||
},
|
||||
"observedVmRegressionPercent": 0.024310531,
|
||||
"targetedTestsPassed": 7,
|
||||
"acceptance": {
|
||||
"status": "rejected_and_removed_before_commit",
|
||||
"reason": "generation checks exactly offset slot initialization savings in the existing representative VM benchmark"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"schemaVersion": "fidc-post-hotpath-full-minute-regression/v1",
|
||||
"measuredAt": "2026-09-05T04:12:00+08:00",
|
||||
"host": "192.168.31.177",
|
||||
"engineCommit": "1f8a0fd",
|
||||
"runtimeCodeCommit": "cfb19b5783cb446099cb3e4aff70cc39beec2e88",
|
||||
"runnerBinarySha256": "3e69af42e41321d31c69b552cf22d7033ce1ea8d94305a32e32461148cdbfc60",
|
||||
"contract": {
|
||||
"startDate": "2025-01-02",
|
||||
"endDate": "2025-11-17",
|
||||
"frequency": "1m",
|
||||
"matchingType": "minute_last",
|
||||
"scheduleTime": "10:18:00",
|
||||
"slippageModel": "price_ratio",
|
||||
"slippageValue": 0.001,
|
||||
"commissionRate": 0.0001,
|
||||
"minimumCommission": 5.0,
|
||||
"stampTaxRate": 0.0005,
|
||||
"volumeLimit": false,
|
||||
"configuredVolumePercentInactive": 0.25
|
||||
},
|
||||
"bundle": {
|
||||
"oldBundleRejectedAsStale": true,
|
||||
"oldBundleId": "bt_bundle_d3109a7220681b850f31",
|
||||
"newBundleId": "bt_bundle_daadb1059454b30a9a6d",
|
||||
"newBundleHash": "14024f3774efb800575c7bd47b6a594b7b7fd7731493a1785c119ee7533378d2",
|
||||
"newDataEpoch": "strategy-factory-source-lake:scope-v1:3556e7dbadbdc94176d23a92acc78c84c50e2ec2a6d16b9dfd25978b1a98b2ae"
|
||||
},
|
||||
"result": {
|
||||
"totalReturn": 0.03473222656500008,
|
||||
"tradeCount": 156,
|
||||
"buyTradeCount": 79,
|
||||
"sellTradeCount": 77,
|
||||
"canonicalSha256": "457c086b1bca784fe83f447bb22451925e8f022456bb0fc7b54237a7a7886849",
|
||||
"resultStoreSha256": "04d804ee02ad7bc9b3649b4a2b4ddd902a4242ef38e00edeed8058860e9a6997",
|
||||
"oldCanonicalSha256": "457c086b1bca784fe83f447bb22451925e8f022456bb0fc7b54237a7a7886849",
|
||||
"canonicalEqualAcrossDataEpoch": true,
|
||||
"terminalAuditStatus": "clean",
|
||||
"warnings": []
|
||||
},
|
||||
"performance": {
|
||||
"coldTotalSeconds": 106.497,
|
||||
"coldDataSeconds": 37.21,
|
||||
"coldEngineSeconds": 65.519,
|
||||
"coldRunContended": true,
|
||||
"hotTotalSeconds": 0.584,
|
||||
"hotDataSeconds": 0.004,
|
||||
"hotEngineSeconds": 0.162,
|
||||
"hotResultSeconds": 0.005,
|
||||
"hotFinalizationSeconds": 0.397
|
||||
},
|
||||
"whiteBoxAudit": {
|
||||
"status": "ok",
|
||||
"sourceRowsFormat": "arrow",
|
||||
"queriedSymbols": 1,
|
||||
"requiredPairs": 156,
|
||||
"queriedDailyBars": 156,
|
||||
"queriedMinuteBars": 156,
|
||||
"slippagePriceChecks": 156,
|
||||
"missingExecutionTimestamps": 0,
|
||||
"executionTimestamp": "each trade date at 10:18:00",
|
||||
"failureCounts": {},
|
||||
"warningCounts": {},
|
||||
"firstBuy": {
|
||||
"symbol": "000001.SZ",
|
||||
"rawMinuteClose": 11.55,
|
||||
"fillPrice": 11.56155,
|
||||
"quantity": 43200,
|
||||
"grossAmount": 499458.96,
|
||||
"commission": 49.945896,
|
||||
"stampTax": 0.0
|
||||
},
|
||||
"lastSell": {
|
||||
"symbol": "000001.SZ",
|
||||
"rawMinuteClose": 11.77,
|
||||
"fillPrice": 11.75823,
|
||||
"quantity": 400,
|
||||
"grossAmount": 4703.292,
|
||||
"commission": 5.0,
|
||||
"stampTax": 2.351646
|
||||
}
|
||||
},
|
||||
"remoteArtifacts": [
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/post-daily-hotpath-full-minute-current-bundle-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/post-daily-hotpath-full-minute-audit-20260905/audit.json"
|
||||
],
|
||||
"acceptance": {
|
||||
"status": "passed",
|
||||
"reason": "the current Source Lake generation reproduces the historical canonical result and every fill passes minute timestamp, raw-price, slippage, fee, tax, and terminal-state audit"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"schemaVersion": "fidc-rank-expression-presence/v1",
|
||||
"measuredAt": "2026-09-05T12:50:00+08:00",
|
||||
"host": "192.168.31.177",
|
||||
"engineCommit": "42999ffa2cb6967315dc4c97220561c0611cda22",
|
||||
"featureCommit": "c225d8484f0493a473b68c11389513d5167ba4cc",
|
||||
"serviceCommit": "9fd5a9e6d5668af57f6942fc3c4127953545d9c6",
|
||||
"runnerBinarySha256": "3060dd5afe720bccd1d2f959f6af04af6325766f8c6829cd80e5af9785ec2346",
|
||||
"implementation": {
|
||||
"description": "freeze rank expression presence at strategy construction and remove repeated rank_expr trim checks in selection dispatch",
|
||||
"strategyCodeChangeRecompilesRequirement": true,
|
||||
"rankValuesCached": false,
|
||||
"selectionResultsCached": false,
|
||||
"pitSemanticsChanged": false
|
||||
},
|
||||
"primaryFiveYearContract": {
|
||||
"totalReturn": 0.9219861819172002,
|
||||
"tradeCount": 26088,
|
||||
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
|
||||
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
|
||||
"previousAcceptedEngineMedianSeconds": 2.611,
|
||||
"engineSeconds": [2.439, 2.447, 2.435, 2.442, 2.441, 2.432],
|
||||
"processHotMedianEngineSeconds": 2.441,
|
||||
"observedImprovementPercent": 6.510915,
|
||||
"resultConsistent": true,
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"secondaryFiveYearContract": {
|
||||
"totalReturn": 1.1342962298106998,
|
||||
"tradeCount": 19404,
|
||||
"canonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
|
||||
"resultStoreSha256": "416d2f87241fb4c6b917f6aeecb588f82f6e7d51a103f4c53a74d11971f16839",
|
||||
"previousAcceptedEngineMedianSeconds": 2.346,
|
||||
"engineSeconds": [2.191, 2.165, 2.249, 2.17, 2.159, 2.18],
|
||||
"processHotMedianEngineSeconds": 2.17,
|
||||
"observedImprovementPercent": 7.502131,
|
||||
"resultConsistent": true,
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"genericRankFiveYearContract": {
|
||||
"rankBy": "free_float_cap",
|
||||
"totalReturn": 0.7140315244542004,
|
||||
"tradeCount": 21876,
|
||||
"canonicalSha256": "84367993911b42437939aad88f29379d007af6f5f171667ff7ad6677dd488fd2",
|
||||
"resultStoreSha256": "e2c7401bb9950da6fdfc0b516428061651e3e5b874a5f80c4b03ab8a81e90191",
|
||||
"lowLoadEngineSeconds": [3.529, 3.472, 3.514, 3.465, 3.455, 3.459],
|
||||
"processHotMedianEngineSeconds": 3.465,
|
||||
"sameLoadCausalComparison": "not_claimed",
|
||||
"resultConsistent": true,
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"fullMinuteContract": {
|
||||
"scheduleTime": "10:18",
|
||||
"matchingType": "minute_last",
|
||||
"slippageModel": "price_ratio",
|
||||
"slippageValue": 0.001,
|
||||
"totalReturn": 0.03473222656500008,
|
||||
"tradeCount": 156,
|
||||
"canonicalSha256": "457c086b1bca784fe83f447bb22451925e8f022456bb0fc7b54237a7a7886849",
|
||||
"resultStoreSha256": "04d804ee02ad7bc9b3649b4a2b4ddd902a4242ef38e00edeed8058860e9a6997",
|
||||
"engineSeconds": [0.157, 0.16],
|
||||
"processHotMedianEngineSeconds": 0.16,
|
||||
"resultConsistent": true,
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"testGate": {
|
||||
"workspaceTotal": 544,
|
||||
"passed": 536,
|
||||
"ignoredManualBenchmarks": 8,
|
||||
"failed": 0
|
||||
},
|
||||
"memory": {
|
||||
"persistentCacheAddedBytes": 0,
|
||||
"currentServiceCgroupBytesAfterRestart": 23715840
|
||||
},
|
||||
"acceptance": {
|
||||
"status": "accepted_compile_time_rank_presence",
|
||||
"reason": "both independent five-year ordered contracts improved in the low-load runs, generic rank stayed business-identical without a causal wall-time claim, and full-minute execution stayed identical"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"schemaVersion": "fidc-rolling-boundary-reuse-rejection/v1",
|
||||
"measuredAt": "2026-09-05T12:40:00+08:00",
|
||||
"host": "192.168.31.177",
|
||||
"candidateCommit": "6b5d576",
|
||||
"rollbackCommit": "b55ac0bf814641ec09f61f4ec10d52b311ee8aab",
|
||||
"finalRemovalCommit": "b55ac0bf814641ec09f61f4ec10d52b311ee8aab",
|
||||
"implementation": {
|
||||
"description": "share one adjusted-close base factor and one valid-volume count across batched rolling windows",
|
||||
"businessResultsChanged": false,
|
||||
"retainedInProduction": false
|
||||
},
|
||||
"primaryFiveYearContract": {
|
||||
"candidateEngineSeconds": [2.6, 2.557, 2.564, 2.603, 2.574, 2.614],
|
||||
"candidateProcessHotMedianEngineSeconds": 2.574,
|
||||
"acceptedBaselineEngineSeconds": 2.441,
|
||||
"observedRegressionPercent": 5.448587,
|
||||
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
|
||||
"tradeCount": 26088
|
||||
},
|
||||
"secondaryFiveYearContract": {
|
||||
"candidateEngineSeconds": [2.381, 2.323, 2.344, 2.377, 2.353, 2.348],
|
||||
"candidateProcessHotMedianEngineSeconds": 2.348,
|
||||
"acceptedBaselineEngineSeconds": 2.17,
|
||||
"observedRegressionPercent": 8.202765,
|
||||
"canonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
|
||||
"tradeCount": 19404
|
||||
},
|
||||
"acceptance": {
|
||||
"status": "rejected_cross_strategy_regression",
|
||||
"reason": "both real five-year controls became slower despite exact result parity; candidate code and production binary were removed"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"schemaVersion": "fidc-selection-band-precheck-rejection/v1",
|
||||
"measuredAt": "2026-09-05T12:55:00+08:00",
|
||||
"host": "192.168.31.177",
|
||||
"candidateCommit": "e060af380ebfce6d9e802bb6e9677c11835fc0d0",
|
||||
"removalCommit": "42999ffa2cb6967315dc4c97220561c0611cda22",
|
||||
"implementation": {
|
||||
"description": "evaluate factor-backed market-cap band bounds before constructing StockExpressionState",
|
||||
"genericRankBenefitObserved": true,
|
||||
"orderedControlRegressionObserved": true,
|
||||
"retainedInProduction": false
|
||||
},
|
||||
"genericRankFiveYearContract": {
|
||||
"rankBy": "free_float_cap",
|
||||
"candidateProcessHotMedianEngineSeconds": 2.835,
|
||||
"acceptedBaselineProcessHotMedianEngineSeconds": 3.465,
|
||||
"observedImprovementPercent": 18.181818,
|
||||
"canonicalSha256": "84367993911b42437939aad88f29379d007af6f5f171667ff7ad6677dd488fd2",
|
||||
"tradeCount": 21876
|
||||
},
|
||||
"primaryFiveYearContract": {
|
||||
"candidateProcessHotMedianEngineSeconds": 2.565,
|
||||
"acceptedBaselineProcessHotMedianEngineSeconds": 2.441,
|
||||
"observedRegressionPercent": 5.079148,
|
||||
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
|
||||
"tradeCount": 26088
|
||||
},
|
||||
"secondaryFiveYearContract": {
|
||||
"candidateProcessHotMedianEngineSeconds": 2.3465,
|
||||
"acceptedBaselineProcessHotMedianEngineSeconds": 2.17,
|
||||
"observedRegressionPercent": 8.133641,
|
||||
"canonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
|
||||
"tradeCount": 19404
|
||||
},
|
||||
"fullMinuteRollbackContract": {
|
||||
"tradeCount": 156,
|
||||
"canonicalSha256": "457c086b1bca784fe83f447bb22451925e8f022456bb0fc7b54237a7a7886849",
|
||||
"resultStoreSha256": "04d804ee02ad7bc9b3649b4a2b4ddd902a4242ef38e00edeed8058860e9a6997",
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"acceptance": {
|
||||
"status": "rejected_cross_strategy_regression",
|
||||
"reason": "single generic-rank improvement cannot justify regressions in both standard ordered controls; candidate was removed from local, remote and deployed code"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"schemaVersion": "fidc-engine-optimization-ab/v1",
|
||||
"generatedDate": "2026-09-07",
|
||||
"host": "192.168.31.177",
|
||||
"change": {
|
||||
"engineCommit": "f45b3a71fa78ecb95b0fa80687130ae7385549e8",
|
||||
"description": "Do not clone the shared multi-date execution quote map when a run reaches the end of one execution date.",
|
||||
"dataContract": "The prepared-data cache remains immutable and shared. A uniquely owned run-local quote map still removes completed dates.",
|
||||
"cachedBusinessResults": false
|
||||
},
|
||||
"deployment": {
|
||||
"serviceCommit": "11cfe1b8d09c1da4807400f3d903199ea6f1e711",
|
||||
"implementationIdentitySha256": "7dbc839acb26fed98fc84a0bc221c316bf370046780c6ae75a79171fd52e5535",
|
||||
"serviceBinarySha256": "bc9385ef340b8e0b3d0d17a9251e159806107443865361271ee0ccd8dbe1a768",
|
||||
"runnerBinarySha256": "55c321f39518b5821e35ee1ec5320e35f94590be3aeeeac0827df07670e623fc",
|
||||
"serviceUser": "boris",
|
||||
"sourceLakeRestarted": false,
|
||||
"health": "ok"
|
||||
},
|
||||
"currentCloseOneYear": {
|
||||
"before": {
|
||||
"processHotMedianTotalSeconds": 1.548,
|
||||
"processHotMedianEngineSeconds": 1.09
|
||||
},
|
||||
"after": {
|
||||
"runs": 5,
|
||||
"processHotMedianTotalSeconds": 1.238,
|
||||
"processHotMedianEngineSeconds": 0.773,
|
||||
"totalReturn": 0.3201517861398,
|
||||
"tradeCount": 5351,
|
||||
"canonicalResultDigest": "7204c6f41b8e39fbf1af7fc55cd601b80f3427a7aa058394ccd8b0b14ca48eed",
|
||||
"resultStoreDigest": "0b4d24ed5ec2b27cc4135707b4c51c78eb2c3e35a20da8108610778c30c72c73",
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"improvementPercent": {
|
||||
"total": 20.02584,
|
||||
"engine": 29.082569
|
||||
}
|
||||
},
|
||||
"crossModeRegression": {
|
||||
"staticCurrentClose": {
|
||||
"processHotTotalSeconds": 0.866,
|
||||
"processHotEngineSeconds": 0.326,
|
||||
"totalReturn": 0.13228843240310018,
|
||||
"tradeCount": 4445,
|
||||
"canonicalResultDigest": "fdfa855295c0b55bdbe6f39952ead1515e844bf033ced974d3c3ddc037a5d0b1",
|
||||
"resultStoreDigest": "697566645116c76ff837cd36f7f9bbd7ad3eb30510a5b95012fb730d5072d511"
|
||||
},
|
||||
"nextBarOpenFiveYear": {
|
||||
"processHotTotalSeconds": 3.212,
|
||||
"processHotEngineSeconds": 2.597,
|
||||
"totalReturn": 0.9922618879291,
|
||||
"tradeCount": 25827,
|
||||
"canonicalResultDigest": "ac1d167cb1e1073e1d1ecb01e914f94d7560081c1d238e6b4418d86233250719",
|
||||
"resultStoreDigest": "79570e0ae6b07badc1b693dc897dd1381647d259a4fe44ed3e50bc215e2fd088"
|
||||
},
|
||||
"fullMinute": {
|
||||
"processHotTotalSeconds": 0.973,
|
||||
"processHotEngineSeconds": 0.231,
|
||||
"totalReturn": 0.03473222656500008,
|
||||
"tradeCount": 156,
|
||||
"canonicalResultDigest": "7dae3a618932b90d36e9968c027f08a94d69b8fc6a56b1ea0cc1a2cb771d85b8",
|
||||
"resultStoreDigest": "bbbd7080b8fd7f6e8c3a8132499842bd0e4d644dbfdc3e8dbca7d2c0c1381e93"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"beforePath": "/srv/fidc/canonical/run/fidc-private/evidence/current-close-post-preplan-profile-20260907",
|
||||
"afterPath": "/srv/fidc/canonical/run/fidc-private/evidence/current-close-shared-quote-profile-20260907",
|
||||
"beforeSamples": 564,
|
||||
"afterSamples": 408,
|
||||
"lostSamples": 0,
|
||||
"beforeFinding": "Arc::make_mut was reached from remove_execution_quotes_on_date and cloned the shared quote map.",
|
||||
"afterFinding": "The remove_execution_quotes_on_date call chain is absent; release_execution_quotes_on_date accounts for 0.27 percent of sampled cycles."
|
||||
},
|
||||
"verification": {
|
||||
"workspaceCoreTestsPassed": 440,
|
||||
"workspaceCoreTestsIgnored": 6,
|
||||
"workspaceFailures": 0,
|
||||
"allCanonicalDigestsMatched": true,
|
||||
"allResultStoreDigestsMatched": true,
|
||||
"allTerminalAuditsClean": true
|
||||
},
|
||||
"supportingEvidence": [
|
||||
"/Users/boris/WorkSpace/fidc-backtest-service/docs/evidence/current-close-shared-quote-release-20260907.json",
|
||||
"/Users/boris/WorkSpace/fidc-backtest-service/docs/evidence/static-pool-shared-quote-release-20260907.json",
|
||||
"/Users/boris/WorkSpace/fidc-backtest-service/docs/evidence/next-open-shared-quote-release-20260907.json",
|
||||
"/Users/boris/WorkSpace/fidc-backtest-service/docs/evidence/full-minute-shared-quote-release-20260907.json"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"schemaVersion": "fidc-shared-market-cap-order-index-ab/v1",
|
||||
"measuredAt": "2026-09-05T00:43:00+08:00",
|
||||
"host": "192.168.31.177",
|
||||
"engineCommit": "05953f857a3e3844595bb63a684c34a2e44cbaaf",
|
||||
"serviceCommit": "9fd5a9e6d5668af57f6942fc3c4127953545d9c6",
|
||||
"runnerBinarySha256": "188269bf0c2ee65d6c11ed37ddddfdaf86695941e8123f69f950054bea39796e",
|
||||
"implementation": {
|
||||
"description": "build an immutable per-date u32 symbol-id order by market cap in parallel with DataSet construction and reuse it across strategy runs",
|
||||
"riskEvaluationOrderPreserved": true,
|
||||
"riskDiagnosticsOrderPreserved": true,
|
||||
"selectionOrderPreserved": true,
|
||||
"cachedFactorValues": false,
|
||||
"cachedSelectionResults": false,
|
||||
"cachedAccountOrOrderState": false,
|
||||
"pitSemanticsChanged": false,
|
||||
"adjustmentSemanticsChanged": false
|
||||
},
|
||||
"primaryFiveYearContract": {
|
||||
"startDate": "2021-08-23",
|
||||
"endDate": "2026-08-28",
|
||||
"frequency": "1d",
|
||||
"matchingType": "next_bar_open",
|
||||
"totalReturn": 0.9219861819172002,
|
||||
"tradeCount": 26088,
|
||||
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
|
||||
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
|
||||
"firstRun": {
|
||||
"runId": "btr_1788540070941_2799354_0",
|
||||
"totalSeconds": 19.825,
|
||||
"dataSeconds": 13.141,
|
||||
"engineSeconds": 6.024
|
||||
},
|
||||
"hotRuns": [
|
||||
{"runId": "btr_1788540095588_2799354_1", "totalSeconds": 6.664, "engineSeconds": 5.528},
|
||||
{"runId": "btr_1788540107127_2799354_2", "totalSeconds": 6.546, "engineSeconds": 5.742},
|
||||
{"runId": "btr_1788540118939_2799354_3", "totalSeconds": 6.586, "engineSeconds": 5.643},
|
||||
{"runId": "btr_1788540129894_2799354_4", "totalSeconds": 6.591, "engineSeconds": 5.929},
|
||||
{"runId": "btr_1788540141992_2799354_5", "totalSeconds": 6.794, "engineSeconds": 6.145}
|
||||
],
|
||||
"hotMedianEngineSeconds": 5.742,
|
||||
"restoredBaselineHotMedianEngineSeconds": 6.702,
|
||||
"observedMedianImprovementPercent": 14.324082363473588,
|
||||
"terminalAuditStatus": "clean",
|
||||
"resultConsistent": true
|
||||
},
|
||||
"secondaryFiveYearContract": {
|
||||
"description": "same frozen source and execution contract with target positions changed from 30 to 20",
|
||||
"totalReturn": 1.1342962298106998,
|
||||
"tradeCount": 19404,
|
||||
"canonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
|
||||
"resultStoreSha256": "416d2f87241fb4c6b917f6aeecb588f82f6e7d51a103f4c53a74d11971f16839",
|
||||
"engineSeconds": [5.699, 5.545, 5.604, 5.23],
|
||||
"processHotMedianEngineSeconds": 5.545,
|
||||
"terminalAuditStatus": "clean",
|
||||
"resultConsistent": true
|
||||
},
|
||||
"memory": {
|
||||
"baselineCurrentBytes": 11426254848,
|
||||
"candidateCurrentBytes": 11433934848,
|
||||
"candidatePeakBytes": 11435282432,
|
||||
"observedIncreaseBytes": 7680000
|
||||
},
|
||||
"profile": {
|
||||
"runId": "btr_1788540249539_2799354_10",
|
||||
"totalSeconds": 6.35,
|
||||
"engineSeconds": 5.696,
|
||||
"eventCycles": 15734772698,
|
||||
"stockStatePercent": 23.86,
|
||||
"stableUniverseSortPresentInTopProfile": false,
|
||||
"numericVmPercent": 5.29,
|
||||
"adjustedCloseMovingAveragePercent": 4.71,
|
||||
"mallocPercent": 4.33
|
||||
},
|
||||
"testGate": {
|
||||
"coreUnitTotal": 419,
|
||||
"corePassed": 413,
|
||||
"ignoredManualBenchmarks": 6,
|
||||
"failed": 0,
|
||||
"integrationSuitesPassed": true
|
||||
},
|
||||
"remoteArtifacts": [
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/shared-market-cap-order-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/shared-market-cap-order-secondary-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/shared-market-cap-order-profile-20260905/perf.data",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/shared-market-cap-order-profile-20260905/perf-report.txt",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/shared-market-cap-order-profile-20260905/run.json"
|
||||
],
|
||||
"acceptance": {
|
||||
"status": "accepted_shared_immutable_index_end_to_end_no_regression",
|
||||
"reason": "both five-year strategies preserve exact business outputs, the primary stable hot median improves by about 14.3 percent, first-build data time remains at the prior process-cold baseline, and resident memory increases by only about 7.7 MB"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"schemaVersion": "fidc-signal-rolling-scan-rejection/v1",
|
||||
"measuredAt": "2026-09-05T13:45:00+08:00",
|
||||
"host": "192.168.31.177",
|
||||
"candidateCommit": "b65b3ed8f1739f7df84c50ae7993ca2ed3d5857e",
|
||||
"removalCommit": "2aa330786aa45ccdfa88c2dc8a65821d8d7a9606",
|
||||
"implementation": {
|
||||
"description": "replace materialized signal_close values and return values with direct current-series scans for rolling max and return sample standard deviation",
|
||||
"businessResultsChanged": false,
|
||||
"retainedInProduction": false
|
||||
},
|
||||
"genericRankFiveYearContract": {
|
||||
"rankBy": "free_float_cap",
|
||||
"candidateEngineSeconds": [3.707, 3.704, 3.7, 3.736, 3.699, 3.721],
|
||||
"candidateProcessHotMedianEngineSeconds": 3.704,
|
||||
"acceptedBaselineProcessHotMedianEngineSeconds": 3.465,
|
||||
"observedRegressionPercent": 6.897547,
|
||||
"canonicalSha256": "84367993911b42437939aad88f29379d007af6f5f171667ff7ad6677dd488fd2",
|
||||
"tradeCount": 21876
|
||||
},
|
||||
"primaryFiveYearContract": {
|
||||
"candidateEngineSeconds": [2.624, 2.607, 2.604, 2.601, 2.61],
|
||||
"candidateProcessHotMedianEngineSeconds": 2.6055,
|
||||
"acceptedBaselineProcessHotMedianEngineSeconds": 2.441,
|
||||
"observedRegressionPercent": 6.739041,
|
||||
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
|
||||
"tradeCount": 26088
|
||||
},
|
||||
"secondaryFiveYearContract": {
|
||||
"candidateEngineSeconds": [2.349, 2.336, 2.39, 2.365, 2.353],
|
||||
"candidateProcessHotMedianEngineSeconds": 2.359,
|
||||
"acceptedBaselineProcessHotMedianEngineSeconds": 2.17,
|
||||
"observedRegressionPercent": 8.709677,
|
||||
"canonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
|
||||
"tradeCount": 19404
|
||||
},
|
||||
"rollbackFullMinuteContract": {
|
||||
"tradeCount": 156,
|
||||
"canonicalSha256": "457c086b1bca784fe83f447bb22451925e8f022456bb0fc7b54237a7a7886849",
|
||||
"resultStoreSha256": "04d804ee02ad7bc9b3649b4a2b4ddd902a4242ef38e00edeed8058860e9a6997",
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"acceptance": {
|
||||
"status": "rejected_cross_strategy_regression",
|
||||
"reason": "direct scans were slower for generic rank and both daily controls despite exact output parity; candidate and deployed binary were removed"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"schemaVersion": "fidc-specialized-stock-snapshot-source/v1",
|
||||
"measuredAt": "2026-09-05T03:38:00+08:00",
|
||||
"host": "192.168.31.177",
|
||||
"engineCommit": "d79678d8509b40ec085f2fa0a7c1e7cb90c95810",
|
||||
"serviceCommit": "9fd5a9e6d5668af57f6942fc3c4127953545d9c6",
|
||||
"runnerBinarySha256": "3f9ce258d3b055097b0cb9e4ae4df423b3d584420befae4d85cdd7ccc51e38bf",
|
||||
"implementation": {
|
||||
"description": "monomorphize indexed and daily-view stock snapshot sources, reuse execution/factor DailySnapshotView in selection loops, and keep the shared stock-state builder free of runtime Optional lookup branches",
|
||||
"eligibleUniverseRowChanged": false,
|
||||
"runtimeOptionalViewBranch": false,
|
||||
"selectionResultCached": false,
|
||||
"factorValueCached": false,
|
||||
"pitSemanticsChanged": false,
|
||||
"componentBenchmark": {
|
||||
"rows": 6000,
|
||||
"rounds": 200,
|
||||
"indexedSeconds": 0.008542,
|
||||
"dailyViewSeconds": 0.002821,
|
||||
"speedup": 3.027294
|
||||
}
|
||||
},
|
||||
"primaryFiveYearContract": {
|
||||
"startDate": "2021-08-23",
|
||||
"endDate": "2026-08-28",
|
||||
"frequency": "1d",
|
||||
"matchingType": "next_bar_open",
|
||||
"totalReturn": 0.9219861819172002,
|
||||
"tradeCount": 26088,
|
||||
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
|
||||
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
|
||||
"processCold": {
|
||||
"totalSeconds": 17.62,
|
||||
"dataSeconds": 13.03,
|
||||
"engineSeconds": 3.976
|
||||
},
|
||||
"processHotEngineSeconds": [4.085, 3.91, 3.893, 4.125, 3.874],
|
||||
"processHotMedianEngineSeconds": 3.91,
|
||||
"typedHelperBaselineMedianEngineSeconds": 4.729,
|
||||
"observedMedianImprovementPercent": 17.318672023683654,
|
||||
"resultConsistent": true,
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"secondaryFiveYearContract": {
|
||||
"totalReturn": 1.1342962298106998,
|
||||
"tradeCount": 19404,
|
||||
"canonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
|
||||
"resultStoreSha256": "416d2f87241fb4c6b917f6aeecb588f82f6e7d51a103f4c53a74d11971f16839",
|
||||
"engineSeconds": [3.672, 3.655, 3.676, 3.689],
|
||||
"medianEngineSeconds": 3.676,
|
||||
"typedHelperBaselineMedianEngineSeconds": 4.049,
|
||||
"observedMedianImprovementPercent": 9.212151148431715,
|
||||
"resultConsistent": true,
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"profile": {
|
||||
"runId": "btr_1788550694645_3730056_10",
|
||||
"engineSeconds": 4.278,
|
||||
"allThreadEventCycles": 95711324146,
|
||||
"comparisonAllThreadEventCycles": 110359308122,
|
||||
"observedCycleReductionPercent": 13.272993665207602,
|
||||
"marketBySymbolIdPercent": 1.69,
|
||||
"factorBySymbolIdPercent": 0.13,
|
||||
"candidateBySymbolIdPercent": 0.32,
|
||||
"comparisonMarketBySymbolIdPercent": 3.46,
|
||||
"comparisonFactorBySymbolIdPercent": 2.66,
|
||||
"comparisonCandidateBySymbolIdPercent": 2.51
|
||||
},
|
||||
"memory": {
|
||||
"serviceCgroupCurrentBytes": 11498143744,
|
||||
"serviceCgroupPeakBytes": 11501256704,
|
||||
"processRssKiB": 11216788,
|
||||
"processAnonymousKiB": 11200492,
|
||||
"typedHelperBaselineCgroupCurrentBytes": 11495362560,
|
||||
"observedCgroupIncreaseBytes": 2781184
|
||||
},
|
||||
"testGate": {
|
||||
"coreUnitTotal": 421,
|
||||
"corePassed": 415,
|
||||
"ignoredManualBenchmarks": 6,
|
||||
"failed": 0,
|
||||
"integrationSuitesPassed": true
|
||||
},
|
||||
"remoteArtifacts": [
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/specialized-stock-snapshot-source-primary-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/specialized-stock-snapshot-source-secondary-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/specialized-stock-snapshot-source-profile-20260905/perf.data",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/specialized-stock-snapshot-source-profile-20260905/perf-report.txt"
|
||||
],
|
||||
"acceptance": {
|
||||
"status": "accepted_generic_monomorphized_snapshot_source",
|
||||
"reason": "both five-year strategies preserve exact outputs and improve stable medians, lookup hotspots fall materially, and the implementation avoids the enlarged row and runtime Optional branches that caused the prior rejected design"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"schemaVersion": "fidc-stock-snapshot-field-projection/v1",
|
||||
"measuredAt": "2026-09-05T02:11:00+08:00",
|
||||
"host": "192.168.31.177",
|
||||
"engineCommit": "7f17fa1fb427fefe5c70c9170fe98e2a8761e3ac",
|
||||
"serviceCommit": "9fd5a9e6d5668af57f6942fc3c4127953545d9c6",
|
||||
"runnerBinarySha256": "0c0ea8629afa789024a6212bcb7ac0e8291f6a88943dd9f7bee16d4902c96be1",
|
||||
"implementation": {
|
||||
"description": "compile per-strategy requirements for amount and upper/lower limit-touch fields, then skip their factor-map lookups and fallback calculations when no expression consumes them",
|
||||
"dynamicFactorMapStillLoadsAllFields": true,
|
||||
"explicitActionsStillLoadAllFields": true,
|
||||
"factorValuesCached": false,
|
||||
"selectionResultsCached": false,
|
||||
"pitSemanticsChanged": false,
|
||||
"executionSemanticsChanged": false
|
||||
},
|
||||
"primaryFiveYearContract": {
|
||||
"startDate": "2021-08-23",
|
||||
"endDate": "2026-08-28",
|
||||
"frequency": "1d",
|
||||
"matchingType": "next_bar_open",
|
||||
"totalReturn": 0.9219861819172002,
|
||||
"tradeCount": 26088,
|
||||
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
|
||||
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
|
||||
"processCold": {
|
||||
"totalSeconds": 18.368,
|
||||
"dataSeconds": 12.691,
|
||||
"engineSeconds": 4.952
|
||||
},
|
||||
"processHotEngineSeconds": [5.434, 4.762, 5.269, 5.366, 5.356],
|
||||
"processHotMedianEngineSeconds": 5.356,
|
||||
"compactKeyBaselineMedianEngineSeconds": 5.734,
|
||||
"observedMedianImprovementPercent": 6.592256714335544,
|
||||
"resultConsistent": true,
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"secondaryFiveYearContract": {
|
||||
"totalReturn": 1.1342962298106998,
|
||||
"tradeCount": 19404,
|
||||
"canonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
|
||||
"resultStoreSha256": "416d2f87241fb4c6b917f6aeecb588f82f6e7d51a103f4c53a74d11971f16839",
|
||||
"lowContentionEngineSeconds": [4.867, 4.36],
|
||||
"highContentionEngineSecondsExcluded": [32.561, 24.001],
|
||||
"resultConsistent": true,
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"hardwareCounters": {
|
||||
"candidateRunId": "btr_1788545432236_3086752_11",
|
||||
"candidateEngineSeconds": 5.291,
|
||||
"candidateInstructions": 26663937176,
|
||||
"candidateBranches": 4751846020,
|
||||
"candidateBranchMisses": 21422590,
|
||||
"internedSymbolBaselineInstructions": 27997441984,
|
||||
"internedSymbolBaselineBranches": 4947191270,
|
||||
"observedInstructionReductionPercent": 4.762952303864304,
|
||||
"observedBranchReductionPercent": 3.9486092075029067
|
||||
},
|
||||
"profile": {
|
||||
"runId": "btr_1788545382848_3086752_10",
|
||||
"engineSeconds": 5.009,
|
||||
"eventCycles": 13976422355,
|
||||
"stockStateSelfPercent": 12.52,
|
||||
"internedSymbolBaselineStockStatePercent": 16.25,
|
||||
"stockStateRelativeReductionPercent": 22.953846153846154,
|
||||
"factorMapBTreeGetBaselinePercent": 4.0,
|
||||
"factorMapBTreeGetPresentInCandidateTopProfile": false
|
||||
},
|
||||
"testGate": {
|
||||
"coreUnitTotal": 421,
|
||||
"corePassed": 415,
|
||||
"ignoredManualBenchmarks": 6,
|
||||
"failed": 0,
|
||||
"integrationSuitesPassed": true
|
||||
},
|
||||
"remoteArtifacts": [
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/stock-snapshot-field-projection-primary-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/stock-snapshot-field-projection-secondary-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/stock-snapshot-field-projection-profile-20260905/perf.data",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/stock-snapshot-field-projection-profile-20260905/perf-report.txt",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/stock-snapshot-field-projection-perf-stat-20260905/perf-stat.csv"
|
||||
],
|
||||
"acceptance": {
|
||||
"status": "accepted_generic_compiled_field_projection",
|
||||
"reason": "two distinct five-year contracts preserve exact business outputs and terminal audits, while the primary low-contention median, hardware counters, and profile all show less work without adding a shared mutable cache"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"schemaVersion": "fidc-stock-state-calendar-index-reuse-ab/v1",
|
||||
"measuredAt": "2026-09-05T00:02:00+08:00",
|
||||
"host": "192.168.31.177",
|
||||
"engineCommit": "6d458dbbc6fa3acb1bff0824307281c82170031a",
|
||||
"serviceCommit": "9fd5a9e6d5668af57f6942fc3c4127953545d9c6",
|
||||
"serviceBinarySha256": "bcd918407de2aa3fdc230d8e9976be5cbf5d3b53185f2a528d55beb10f5f1552",
|
||||
"runnerBinarySha256": "e33dd75aa7b481975c0991cf634d07900cdefa51756802e28e64f18b980a83dc",
|
||||
"implementation": {
|
||||
"description": "resolve the trading-calendar index once when the per-day stock-state cache advances, then reuse it for decision and current rolling-series boundaries",
|
||||
"cachedFactorValues": false,
|
||||
"cachedSelectionResults": false,
|
||||
"cachedOrderState": false,
|
||||
"pitSemanticsChanged": false,
|
||||
"adjustmentSemanticsChanged": false,
|
||||
"fallback": "dates outside the indexed calendar retain the existing date-based rolling fallback"
|
||||
},
|
||||
"componentBenchmark": {
|
||||
"samples": 6,
|
||||
"iterationsPerSample": 5000000,
|
||||
"dateLookupSeconds": 0.400282218,
|
||||
"reusedCalendarIndexSeconds": 0.070751718,
|
||||
"speedup": 5.657561813552005,
|
||||
"checksum": 90000000,
|
||||
"equal": true
|
||||
},
|
||||
"testGate": {
|
||||
"coreUnitTotal": 419,
|
||||
"corePassed": 413,
|
||||
"ignoredManualBenchmarks": 6,
|
||||
"failed": 0,
|
||||
"integrationSuitesPassed": true
|
||||
},
|
||||
"primaryFiveYearContract": {
|
||||
"startDate": "2021-08-23",
|
||||
"endDate": "2026-08-28",
|
||||
"frequency": "1d",
|
||||
"matchingType": "next_bar_open",
|
||||
"totalReturn": 0.9219861819172002,
|
||||
"tradeCount": 26088,
|
||||
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
|
||||
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
|
||||
"beforeEngineSeconds": [6.764, 6.391, 12.387],
|
||||
"beforeMedianEngineSeconds": 6.764,
|
||||
"afterHotEngineSeconds": [6.224, 6.497, 6.405, 6.539, 6.386],
|
||||
"afterMedianEngineSeconds": 6.405,
|
||||
"observedMedianImprovementPercent": 5.307510348905976,
|
||||
"benchmarkSummaryProcessHotMedianSeconds": {
|
||||
"before": 9.389,
|
||||
"after": 6.451
|
||||
},
|
||||
"note": "The explicit all-run medians are reported above. The benchmark tool excludes its first run when calculating processHotMedian; concurrent non-FIDC load makes the tool summary less comparable than the complete sample list."
|
||||
},
|
||||
"secondaryFiveYearContract": {
|
||||
"description": "same frozen source and execution contract with target positions changed from 30 to 20",
|
||||
"totalReturn": 1.1342962298106998,
|
||||
"tradeCount": 19404,
|
||||
"canonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
|
||||
"resultStoreSha256": "416d2f87241fb4c6b917f6aeecb588f82f6e7d51a103f4c53a74d11971f16839",
|
||||
"engineSeconds": [6.217, 9.279, 10.771],
|
||||
"resultConsistent": true,
|
||||
"performanceClaimed": false,
|
||||
"reason": "two runs overlapped high external host contention; the strategy is retained as cross-strategy semantic evidence only"
|
||||
},
|
||||
"excludedHighContentionBatch": {
|
||||
"hostLoadAverageObserved": [65.08, 44.49, 35.56],
|
||||
"engineSeconds": [11.601, 12.061, 33.904, 23.149, 7.391],
|
||||
"resultConsistent": true,
|
||||
"performanceComparisonExcluded": true,
|
||||
"reason": "independent tan processes and active FIDC symbolic workers caused material host scheduling contention; no external process was modified"
|
||||
},
|
||||
"runtime": {
|
||||
"servicePid": 2548679,
|
||||
"serviceUser": "boris",
|
||||
"serviceState": "active/running",
|
||||
"allowedCpus": "0 2 4 6 8 10 12 14 48 50 52 54 56 58 60 62",
|
||||
"memoryCurrentBytes": 11426254848,
|
||||
"memoryPeakBytes": 11427790848,
|
||||
"maxConcurrentRuns": 1,
|
||||
"runnerThreadsPerRun": 8
|
||||
},
|
||||
"remoteArtifacts": [
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/calendar-index-before-20260904.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/calendar-index-after-20260904.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/calendar-index-after-hot-20260904.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/calendar-index-secondary-20260905.json"
|
||||
],
|
||||
"acceptance": {
|
||||
"status": "accepted_component_and_primary_end_to_end_no_regression",
|
||||
"reason": "the component path is materially faster, the stable primary five-year batch improves, and both frozen business contracts preserve exact returns, trade counts, canonical digests, result-store digests, and clean terminal audits"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"schemaVersion": "fidc-quote-plan-optimization-ab/v1",
|
||||
"generatedDate": "2026-09-07",
|
||||
"host": "192.168.31.177",
|
||||
"implementation": {
|
||||
"engineCommit": "c934a948c6b3adc0d38b99cf5826481a9a7c3ceb",
|
||||
"serviceCommit": "178cd2a76961b7e687424cc47107c2c1c3f4f74e",
|
||||
"implementationIdentitySha256": "192392bd7c6bbb7d9a8601aca51a55de4a518357d2c3014b4a09e644e7caccc6",
|
||||
"engineChange": "Stream ascending market-cap quote candidates from the shared ordered symbol index without a candidate-state arena or redundant sort.",
|
||||
"runnerChange": "Build the preliminary selection DataSet from daily bundles instead of flattening and regrouping component vectors."
|
||||
},
|
||||
"quotePlanSeconds": {
|
||||
"baselineSamples": [
|
||||
7.143,
|
||||
6.848,
|
||||
7.404
|
||||
],
|
||||
"baselineMedian": 7.143,
|
||||
"candidateSamples": [
|
||||
7.092,
|
||||
6.115,
|
||||
5.931,
|
||||
6.179,
|
||||
6.129,
|
||||
6.096
|
||||
],
|
||||
"candidateMedian": 6.122,
|
||||
"improvementPercent": 14.293714
|
||||
},
|
||||
"dynamicCurrentClose": {
|
||||
"totalReturn": 0.3201517861398,
|
||||
"tradeCount": 5351,
|
||||
"riskDecisionCount": 2661,
|
||||
"canonicalResultDigest": "7204c6f41b8e39fbf1af7fc55cd601b80f3427a7aa058394ccd8b0b14ca48eed",
|
||||
"resultStoreDigest": "0b4d24ed5ec2b27cc4135707b4c51c78eb2c3e35a20da8108610778c30c72c73",
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"crossModeRegression": {
|
||||
"staticCurrentClose": {
|
||||
"totalReturn": 0.13228843240310018,
|
||||
"tradeCount": 4445,
|
||||
"canonicalResultDigest": "fdfa855295c0b55bdbe6f39952ead1515e844bf033ced974d3c3ddc037a5d0b1",
|
||||
"resultStoreDigest": "697566645116c76ff837cd36f7f9bbd7ad3eb30510a5b95012fb730d5072d511"
|
||||
},
|
||||
"fullMinute": {
|
||||
"totalReturn": 0.03473222656500008,
|
||||
"tradeCount": 156,
|
||||
"canonicalResultDigest": "7dae3a618932b90d36e9968c027f08a94d69b8fc6a56b1ea0cc1a2cb771d85b8",
|
||||
"resultStoreDigest": "bbbd7080b8fd7f6e8c3a8132499842bd0e4d644dbfdc3e8dbca7d2c0c1381e93"
|
||||
},
|
||||
"nextBarOpenFiveYear": {
|
||||
"totalReturn": 0.9922618879291,
|
||||
"tradeCount": 25827,
|
||||
"canonicalResultDigest": "ac1d167cb1e1073e1d1ecb01e914f94d7560081c1d238e6b4418d86233250719",
|
||||
"resultStoreDigest": "79570e0ae6b07badc1b693dc897dd1381647d259a4fe44ed3e50bc215e2fd088"
|
||||
}
|
||||
},
|
||||
"testGate": {
|
||||
"engineCorePassed": 441,
|
||||
"engineIgnoredBenchmarks": 6,
|
||||
"runnerPassed": 347,
|
||||
"runnerIgnoredBenchmarks": 3,
|
||||
"apiPassed": 80,
|
||||
"failures": 0,
|
||||
"fastPathGenericPathParityTest": true
|
||||
},
|
||||
"remainingBottleneck": {
|
||||
"profilePath": "/srv/fidc/canonical/run/fidc-private/evidence/daily-bundle-quote-plan-cold-profile-20260907",
|
||||
"dataSeconds": 11.215,
|
||||
"quotePlanSeconds": 6.096,
|
||||
"datasetConstructSeconds": 1.677,
|
||||
"loopSeconds": 2.28,
|
||||
"finding": "SymbolPriceSeries, adjusted-close series and DataSet indexes are still built once for quote planning and again for the final run DataSet.",
|
||||
"next": "Replace SourceRowRecord and duplicate preliminary/final DataSet construction with one epoch-scoped typed Base Panel and lightweight run views."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"schemaVersion": "fidc-symbol-id-ranked-candidates/v1",
|
||||
"measuredAt": "2026-09-05T06:23:00+08:00",
|
||||
"host": "192.168.31.177",
|
||||
"engineCommit": "52f9ee9d92781c1367ba4f01cbc3b1676fdd241e",
|
||||
"serviceCommit": "9fd5a9e6d5668af57f6942fc3c4127953545d9c6",
|
||||
"runnerBinarySha256": "c567c6da3d3625839a1d0127f755ccd278f31473924bf81ab837158370c08667",
|
||||
"implementation": {
|
||||
"description": "rank custom and quote-plan candidates as (symbol_id, rank, state_index), borrow symbols only at diagnostics and output boundaries, and use the lexical symbol-id order as the exact equal-rank tie breaker",
|
||||
"symbolIdContract": "DataSet assigns u32 ids after sorting the complete normalized symbol set lexicographically",
|
||||
"candidateSymbolClonesRemoved": true,
|
||||
"selectionResultCached": false,
|
||||
"persistentCacheAdded": false,
|
||||
"rankTieSemanticsChanged": false,
|
||||
"pitSemanticsChanged": false
|
||||
},
|
||||
"rejectedIntermediate": {
|
||||
"commit": "d7e11be",
|
||||
"problem": "looked up both factor-row strings inside every equal-rank comparator call even though symbol ids already encode lexical order",
|
||||
"engineSeconds": [6.056, 6.274, 5.298, 6.071, 6.066, 5.957],
|
||||
"retainedInFinalCode": false
|
||||
},
|
||||
"genericRankFiveYearContract": {
|
||||
"rankBy": "free_float_cap",
|
||||
"totalReturn": 0.7140315244542004,
|
||||
"tradeCount": 21876,
|
||||
"canonicalSha256": "84367993911b42437939aad88f29379d007af6f5f171667ff7ad6677dd488fd2",
|
||||
"resultStoreSha256": "e2c7401bb9950da6fdfc0b516428061651e3e5b874a5f80c4b03ab8a81e90191",
|
||||
"arenaBaselineMedianEngineSeconds": 4.222,
|
||||
"engineSeconds": [3.809, 3.685, 3.739, 3.743, 3.76, 3.752],
|
||||
"processHotMedianEngineSeconds": 3.743,
|
||||
"incrementalImprovementPercent": 11.345334,
|
||||
"originalCachedBaselineMedianEngineSeconds": 5.934,
|
||||
"cumulativeImprovementPercent": 36.922818,
|
||||
"resultConsistent": true,
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"primaryOrderedFiveYearContract": {
|
||||
"totalReturn": 0.9219861819172002,
|
||||
"tradeCount": 26088,
|
||||
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
|
||||
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
|
||||
"previousProcessHotMedianEngineSeconds": 2.73,
|
||||
"processHotMedianEngineSeconds": 2.611,
|
||||
"observedImprovementPercent": 4.358974,
|
||||
"resultConsistent": true,
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"secondaryOrderedFiveYearContract": {
|
||||
"totalReturn": 1.1342962298106998,
|
||||
"tradeCount": 19404,
|
||||
"canonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
|
||||
"resultStoreSha256": "416d2f87241fb4c6b917f6aeecb588f82f6e7d51a103f4c53a74d11971f16839",
|
||||
"previousProcessHotMedianEngineSeconds": 2.372,
|
||||
"processHotMedianEngineSeconds": 2.346,
|
||||
"observedImprovementPercent": 1.096121,
|
||||
"resultConsistent": true,
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"fullMinuteContract": {
|
||||
"scheduleTime": "10:18",
|
||||
"matchingType": "minute_last",
|
||||
"slippageModel": "price_ratio",
|
||||
"slippageValue": 0.001,
|
||||
"totalReturn": 0.03473222656500008,
|
||||
"tradeCount": 156,
|
||||
"canonicalSha256": "457c086b1bca784fe83f447bb22451925e8f022456bb0fc7b54237a7a7886849",
|
||||
"resultStoreSha256": "04d804ee02ad7bc9b3649b4a2b4ddd902a4242ef38e00edeed8058860e9a6997",
|
||||
"totalSeconds": [0.608, 0.608],
|
||||
"engineSeconds": [0.161, 0.167],
|
||||
"resultConsistent": true,
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"profile": {
|
||||
"runId": "btr_1788560495042_846890_19",
|
||||
"engineSeconds": 4.007,
|
||||
"allThreadEventCycles": 86456699303,
|
||||
"transientStockStateBuilderPercent": 18.97,
|
||||
"numericVmEvaluatePercent": 8.0,
|
||||
"rankedSelectionPercent": 7.6,
|
||||
"adjustedCloseMovingAveragePercent": 5.38,
|
||||
"stableSortPercent": 3.09,
|
||||
"mallocPercent": 1.19,
|
||||
"arenaBaselineMallocPercent": 3.34,
|
||||
"lostSamples": 0
|
||||
},
|
||||
"memory": {
|
||||
"serviceCgroupCurrentBytes": 11537428480,
|
||||
"serviceCgroupPeakBytes": 11538923520,
|
||||
"persistentCacheAddedBytes": 0
|
||||
},
|
||||
"testGate": {
|
||||
"workspaceTotal": 544,
|
||||
"passed": 536,
|
||||
"ignoredManualBenchmarks": 8,
|
||||
"failed": 0,
|
||||
"lexicalSymbolIdRegression": true
|
||||
},
|
||||
"remoteArtifacts": [
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/symbol-id-rank-tie-fixed-generic-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/symbol-id-rank-tie-fixed-primary-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/symbol-id-rank-tie-fixed-secondary-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/symbol-id-rank-tie-fixed-full-minute-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/symbol-id-rank-profile-20260905/perf.data",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/symbol-id-rank-profile-20260905/perf-report.txt"
|
||||
],
|
||||
"acceptance": {
|
||||
"status": "accepted_symbol_id_ranked_candidates",
|
||||
"reason": "the final implementation preserves exact rank ties and all business outputs, improves the generic five-year median by 11.35 percent over the arena baseline and 36.92 percent over the old cached path, improves both ordered controls, and lowers malloc share without persistent memory"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"schemaVersion": "fidc-symbol-id-selection-stream/v1",
|
||||
"measuredAt": "2026-09-05T04:53:13+08:00",
|
||||
"host": "192.168.31.177",
|
||||
"engineCommit": "0e3c2028d0b0a26a2d3b8e2fb9dd0765c60809b1",
|
||||
"serviceCommit": "9fd5a9e6d5668af57f6942fc3c4127953545d9c6",
|
||||
"runnerBinarySha256": "386e5542d66bc6850f9569dc25d061f043103df279d3aba5e86343a72d5113f9",
|
||||
"implementation": {
|
||||
"description": "scan signal-day selection risk into a market-cap-ordered u32 symbol-id stream and materialize EligibleUniverseSnapshot rows only for strategies that require the generic ranking path",
|
||||
"fastPathContract": "ascending market_cap or market_cap_bn rank with daily_replacement_limit=0",
|
||||
"genericRankingChanged": false,
|
||||
"selectionResultCached": false,
|
||||
"factorValueCached": false,
|
||||
"riskDecisionOrderChanged": false,
|
||||
"pitSemanticsChanged": false
|
||||
},
|
||||
"primaryFiveYearContract": {
|
||||
"startDate": "2021-08-23",
|
||||
"endDate": "2026-08-28",
|
||||
"frequency": "1d",
|
||||
"matchingType": "next_bar_open",
|
||||
"totalReturn": 0.9219861819172002,
|
||||
"tradeCount": 26088,
|
||||
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
|
||||
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
|
||||
"restartOrCold": {
|
||||
"totalSeconds": 19.223,
|
||||
"dataSeconds": 14.395,
|
||||
"engineSeconds": 4.182
|
||||
},
|
||||
"processHotEngineSeconds": [3.438, 3.185, 3.346, 3.288, 3.294],
|
||||
"processHotMedianEngineSeconds": 3.294,
|
||||
"previousAcceptedMedianEngineSeconds": 3.896,
|
||||
"observedMedianImprovementPercent": 15.451745,
|
||||
"resultConsistent": true,
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"secondaryFiveYearContract": {
|
||||
"totalReturn": 1.1342962298106998,
|
||||
"tradeCount": 19404,
|
||||
"canonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
|
||||
"resultStoreSha256": "416d2f87241fb4c6b917f6aeecb588f82f6e7d51a103f4c53a74d11971f16839",
|
||||
"engineSeconds": [2.871, 2.897, 2.899, 2.964, 2.948, 2.922],
|
||||
"medianEngineSeconds": 2.922,
|
||||
"previousAcceptedMedianEngineSeconds": 3.582,
|
||||
"observedMedianImprovementPercent": 18.425461,
|
||||
"resultConsistent": true,
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"profile": {
|
||||
"runId": "btr_1788555090941_297399_12",
|
||||
"engineSeconds": 3.37,
|
||||
"allThreadEventCycles": 77490041805,
|
||||
"stockStateBySymbolIdPercent": 13.59,
|
||||
"numericVmEvaluatePercent": 8.27,
|
||||
"adjustedCloseMovingAveragePercent": 4.74,
|
||||
"selectionSymbolIdScanPercent": 3.53,
|
||||
"selectSymbolsPercent": 3.26,
|
||||
"stringTrimPercent": 3.14,
|
||||
"mallocPercent": 2.82,
|
||||
"lostSamples": 0
|
||||
},
|
||||
"memory": {
|
||||
"serviceCgroupCurrentBytes": 11481071616,
|
||||
"serviceCgroupPeakBytes": 11488108544,
|
||||
"cacheMemoryAddedBytes": 0
|
||||
},
|
||||
"testGate": {
|
||||
"workspaceTotal": 544,
|
||||
"passed": 536,
|
||||
"ignoredManualBenchmarks": 8,
|
||||
"failed": 0
|
||||
},
|
||||
"remoteArtifacts": [
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/symbol-id-selection-primary-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/symbol-id-selection-secondary-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/symbol-id-selection-profile-20260905/run.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/symbol-id-selection-profile-20260905/perf.data",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/symbol-id-selection-profile-20260905/perf-report.txt"
|
||||
],
|
||||
"acceptance": {
|
||||
"status": "accepted_generic_symbol_id_selection_stream",
|
||||
"reason": "both independent five-year contracts preserve exact results and improve stable engine medians materially without adding cache memory or changing generic ranking, risk ordering, or PIT semantics"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"schemaVersion": "fidc-symbol-id-series-storage-rejection/v1",
|
||||
"measuredAt": "2026-09-06T06:31:00+08:00",
|
||||
"host": "192.168.31.177",
|
||||
"candidateCommit": "5a7c49a4543b584503ff0d2c2ae513c68d25de8a",
|
||||
"implementation": "remove duplicate string-keyed market and adjusted-close series maps and build symbol-id vectors directly",
|
||||
"businessParity": {
|
||||
"primaryFiveYearCanonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
|
||||
"secondaryFiveYearCanonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
|
||||
"genericRankFiveYearCanonicalSha256": "84367993911b42437939aad88f29379d007af6f5f171667ff7ad6677dd488fd2",
|
||||
"minuteCanonicalSha256": "457c086b1bca784fe83f447bb22451925e8f022456bb0fc7b54237a7a7886849",
|
||||
"allTerminalAuditsClean": true,
|
||||
"allResultsConsistent": true
|
||||
},
|
||||
"performance": {
|
||||
"baselineDatasetConstructSeconds": [4.448, 4.546],
|
||||
"candidateEarlyDatasetConstructSeconds": [3.899, 3.895, 3.663],
|
||||
"candidateFinalRestartDatasetConstructSeconds": 4.472,
|
||||
"candidateFinalRestartColdDataSeconds": 14.513,
|
||||
"candidateFinalRestartColdTotalSeconds": 18.381,
|
||||
"conclusion": "the apparent early improvement did not reproduce after a final restart under the current host phase; the final constructor time is equal to the adjacent baseline range"
|
||||
},
|
||||
"memory": {
|
||||
"baselineSingleDatasetCurrentBytes": 11470495744,
|
||||
"candidateSingleDatasetCurrentBytes": 11473674240,
|
||||
"conclusion": "no measurable resident-memory reduction"
|
||||
},
|
||||
"testGate": {
|
||||
"corePassed": 538,
|
||||
"ignoredManualBenchmarks": 8,
|
||||
"failed": 0
|
||||
},
|
||||
"remoteArtifacts": [
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/symbol-id-series-primary-five-year-20260906.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/symbol-id-series-secondary-five-year-20260906.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/symbol-id-series-generic-rank-five-year-20260906.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/symbol-id-series-minute-20260906.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/final-symbol-id-series-post-doc-sync-20260906.json"
|
||||
],
|
||||
"decision": {
|
||||
"status": "rejected_and_removed",
|
||||
"reason": "the candidate preserved correctness but did not provide a stable end-to-end or memory improvement across restart validation; duplicate maps are not a proven material bottleneck",
|
||||
"nextTarget": "remove the SourceRowRecord to DailySnapshot to DataSet multi-stage materialization, or publish a content-addressed base panel that can be mapped across restarts"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
{
|
||||
"schemaVersion": "fidc-transient-selection-arena/v1",
|
||||
"measuredAt": "2026-09-05T05:57:00+08:00",
|
||||
"host": "192.168.31.177",
|
||||
"engineCommit": "d89dd24f0a3993ed10b00957f947b65d91b00ffc",
|
||||
"serviceCommit": "9fd5a9e6d5668af57f6942fc3c4127953545d9c6",
|
||||
"runnerBinarySha256": "53680c33cd96487c52c47b96bb8c6f0bece69bb5617a6036d382f4262c181b2c",
|
||||
"implementation": {
|
||||
"description": "keep custom-rank and quote-plan stock states in a contiguous transient arena, sort lightweight state indexes, and isolate ordered and generic selection functions",
|
||||
"candidateSnapshotCacheUsed": false,
|
||||
"selectionResultCached": false,
|
||||
"stateArenaSharedAcrossRuns": false,
|
||||
"stateArenaLifetime": "single selection call",
|
||||
"riskDecisionOrderChanged": false,
|
||||
"pitSemanticsChanged": false
|
||||
},
|
||||
"rejectedIntermediate": {
|
||||
"commit": "7f7fce1",
|
||||
"problem": "stored StockExpressionState directly in the sortable tuple and repeatedly moved the large value during stable sorting",
|
||||
"rollbackBaselineEngineSeconds": 5.934,
|
||||
"observedBadCandidateEngineSeconds": [6.972, 8.868, 8.748, 9.317, 6.251],
|
||||
"retainedInFinalCode": false
|
||||
},
|
||||
"genericRankFiveYearContract": {
|
||||
"requestSha256": "06a5cdf6a96673d45cc54f4ff0f18a125dbcc0b80d0d52496f8a0c638bbc81d3",
|
||||
"rankBy": "free_float_cap",
|
||||
"totalReturn": 0.7140315244542004,
|
||||
"tradeCount": 21876,
|
||||
"canonicalSha256": "84367993911b42437939aad88f29379d007af6f5f171667ff7ad6677dd488fd2",
|
||||
"resultStoreSha256": "e2c7401bb9950da6fdfc0b516428061651e3e5b874a5f80c4b03ab8a81e90191",
|
||||
"rollbackMedianEngineSeconds": 5.934,
|
||||
"finalEngineSeconds": [4.143, 4.125, 4.218, 4.226, 4.379],
|
||||
"finalMedianEngineSeconds": 4.222,
|
||||
"observedMedianImprovementPercent": 28.850691,
|
||||
"resultConsistent": true,
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"primaryOrderedFiveYearContract": {
|
||||
"totalReturn": 0.9219861819172002,
|
||||
"tradeCount": 26088,
|
||||
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
|
||||
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
|
||||
"engineSeconds": [2.647, 2.692, 2.659, 2.813, 2.768],
|
||||
"allRunMedianEngineSeconds": 2.692,
|
||||
"previousAcceptedMedianEngineSeconds": 2.667,
|
||||
"observedMedianDeltaPercent": 0.937383,
|
||||
"classification": "within_host_load_variance",
|
||||
"resultConsistent": true,
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"secondaryOrderedFiveYearContract": {
|
||||
"totalReturn": 1.1342962298106998,
|
||||
"tradeCount": 19404,
|
||||
"canonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
|
||||
"resultStoreSha256": "416d2f87241fb4c6b917f6aeecb588f82f6e7d51a103f4c53a74d11971f16839",
|
||||
"rollbackMedianEngineSeconds": 2.408,
|
||||
"finalProcessHotMedianEngineSeconds": 2.372,
|
||||
"observedMedianImprovementPercent": 1.495017,
|
||||
"resultConsistent": true,
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"fullMinuteContract": {
|
||||
"scheduleTime": "10:18",
|
||||
"matchingType": "minute_last",
|
||||
"slippageModel": "price_ratio",
|
||||
"slippageValue": 0.001,
|
||||
"totalReturn": 0.03473222656500008,
|
||||
"tradeCount": 156,
|
||||
"canonicalSha256": "457c086b1bca784fe83f447bb22451925e8f022456bb0fc7b54237a7a7886849",
|
||||
"resultStoreSha256": "04d804ee02ad7bc9b3649b4a2b4ddd902a4242ef38e00edeed8058860e9a6997",
|
||||
"totalSeconds": [0.6, 0.546],
|
||||
"engineSeconds": [0.155, 0.164],
|
||||
"resultConsistent": true,
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"genericRankProfile": {
|
||||
"runId": "btr_1788558913084_672102_18",
|
||||
"engineSeconds": 4.46,
|
||||
"allThreadEventCycles": 33149272847,
|
||||
"transientStockStateBuilderPercent": 16.1,
|
||||
"numericVmEvaluatePercent": 8.53,
|
||||
"rankedSelectionPercent": 6.8,
|
||||
"adjustedCloseMovingAveragePercent": 4.87,
|
||||
"stableSortPercent": 3.68,
|
||||
"mallocPercent": 3.34,
|
||||
"lostSamples": 0
|
||||
},
|
||||
"memory": {
|
||||
"serviceCgroupCurrentBytes": 11510001664,
|
||||
"serviceCgroupPeakBytes": 11511304192,
|
||||
"persistentCacheAddedBytes": 0
|
||||
},
|
||||
"testGate": {
|
||||
"workspaceTotal": 544,
|
||||
"passed": 536,
|
||||
"ignoredManualBenchmarks": 8,
|
||||
"failed": 0,
|
||||
"quotePlanTransientCacheAssertions": true
|
||||
},
|
||||
"remoteArtifacts": [
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/free-float-rank-five-year-request-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/transient-selection-rollback-generic-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/transient-selection-final-generic-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/transient-selection-final-primary-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/transient-selection-rollback-secondary-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/transient-selection-final-secondary-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/transient-selection-final-full-minute-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/transient-selection-final-profile-20260905/perf.data",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/transient-selection-final-profile-20260905/perf-report.txt"
|
||||
],
|
||||
"acceptance": {
|
||||
"status": "accepted_generic_transient_selection_arena",
|
||||
"reason": "the same frozen custom-rank contract is about 28.85 percent faster than an immediate rollback, both ordered five-year contracts and full-minute execution preserve exact outputs, the final design sorts indexes rather than large states, and no persistent cache is added"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
{
|
||||
"schemaVersion": "fidc-typed-adjustment-factor-snapshot/v1",
|
||||
"generatedAt": "2026-09-07T18:12:00+08:00",
|
||||
"scope": "FIDC engine, backtest runner and strategy runtime",
|
||||
"changes": {
|
||||
"engineCommit": "04b45adf98772f4ce3cd8b7a2c08d76480655fee",
|
||||
"backtestServiceCommit": "76748bbc3d2cfc4d76ff8e35c5fcda1fcddd00ec",
|
||||
"tradingPlatformCommit": "bec62d0f7f6126a36ec0a101f7b1a66b138537eb",
|
||||
"datasetSchemaVersion": 58,
|
||||
"sourceRowCacheSchemaVersion": 28,
|
||||
"contract": "adjustment_factor_backward1 remains a nullable typed field from Arrow decode through DailyFactorSnapshot and AdjustedCloseSeries; dynamic NumericFactorMap values stay sparse",
|
||||
"legacyInputPolicy": "extra_factors containing adjustment_factor_backward1 and non-positive or non-finite typed adjustment values are rejected"
|
||||
},
|
||||
"tests": {
|
||||
"fidcCore": "445 passed, 6 ignored",
|
||||
"backtestRunner": "351 passed, 3 ignored",
|
||||
"backtestApi": "80 passed",
|
||||
"strategyRuntime": "66 passed",
|
||||
"runtimeRollingOrderRegression": "full 5/10/30/100-day framework rolling history produced a valid paper order"
|
||||
},
|
||||
"productionImplementation": {
|
||||
"engineCommit": "04b45adf98772f4ce3cd8b7a2c08d76480655fee",
|
||||
"serviceCommit": "76748bbc3d2cfc4d76ff8e35c5fcda1fcddd00ec",
|
||||
"identitySha256": "2c0ab93663018f4fae8d61ba0c522c0f796837d952bbfb26a6bc229cc52bd722",
|
||||
"runnerBinarySha256": "8591dfa763a52e06467dfe2ff2c8bef4d382ef875b356009fe5a7e8ea2013ea3",
|
||||
"serviceBinarySha256": "4e467981f569e265c4ccc862c04be85eb99e9f4d107878785c5961a8b382d65f",
|
||||
"status": "verified"
|
||||
},
|
||||
"fiveYearFreshProcess": {
|
||||
"baseline": {
|
||||
"runId": "btr_1788772852314_617886_0",
|
||||
"dataSeconds": 14.121,
|
||||
"sourceQuerySeconds": 4.304,
|
||||
"datasetConstructSeconds": 3.672,
|
||||
"riskFreeRateSeconds": 2.77,
|
||||
"totalSeconds": 20.947,
|
||||
"queryCompletedRssKb": 6654344,
|
||||
"datasetConstructCompletedRssKb": 12775136
|
||||
},
|
||||
"candidate": {
|
||||
"runId": "btr_1788775107699_632665_0",
|
||||
"dataSeconds": 13.198,
|
||||
"sourceQuerySeconds": 4.169,
|
||||
"datasetConstructSeconds": 3.499,
|
||||
"riskFreeRateSeconds": 0.01,
|
||||
"totalSeconds": 17.381,
|
||||
"queryCompletedRssKb": 6738520,
|
||||
"datasetConstructCompletedRssKb": 12694000
|
||||
},
|
||||
"improvementPercent": {
|
||||
"dataSeconds": 6.5364,
|
||||
"sourceQuerySeconds": 3.1366,
|
||||
"datasetConstructSeconds": 4.7113,
|
||||
"datasetConstructCompletedRssKb": 0.6351,
|
||||
"totalSecondsAfterRiskFreeNormalization": 4.4317
|
||||
},
|
||||
"comparisonNote": "total wall time is normalized only for the independently measured risk-free lookup difference; data and RSS values are compared directly"
|
||||
},
|
||||
"fiveYearMemoryCold": {
|
||||
"baselineRunIds": [
|
||||
"btr_1788772635840_613911_1",
|
||||
"btr_1788772672944_613911_2",
|
||||
"btr_1788772707181_613911_3"
|
||||
],
|
||||
"candidateRunIds": [
|
||||
"btr_1788775166889_632665_1",
|
||||
"btr_1788775202156_632665_2",
|
||||
"btr_1788775353507_632665_5"
|
||||
],
|
||||
"baselineMedian": {
|
||||
"totalSeconds": 16.809,
|
||||
"dataSeconds": 12.8,
|
||||
"sourceQuerySeconds": 4.411,
|
||||
"dailyLoopSeconds": 1.077,
|
||||
"datasetConstructSeconds": 3.701,
|
||||
"engineSeconds": 3.278
|
||||
},
|
||||
"candidateMedian": {
|
||||
"totalSeconds": 16.653,
|
||||
"dataSeconds": 12.569,
|
||||
"sourceQuerySeconds": 4.054,
|
||||
"dailyLoopSeconds": 1.047,
|
||||
"datasetConstructSeconds": 3.577,
|
||||
"engineSeconds": 3.372
|
||||
},
|
||||
"improvementPercent": {
|
||||
"totalSeconds": 0.9281,
|
||||
"dataSeconds": 1.8047,
|
||||
"sourceQuerySeconds": 8.0934,
|
||||
"dailyLoopSeconds": 2.7855,
|
||||
"datasetConstructSeconds": 3.3504,
|
||||
"engineSeconds": -2.8676
|
||||
},
|
||||
"excludedQueueOutlier": {
|
||||
"runId": "btr_1788775236942_632665_4",
|
||||
"totalSeconds": 69.401,
|
||||
"queueWaitSeconds": 48.625,
|
||||
"blockingRunId": "btr_1788775223727_632665_3",
|
||||
"reason": "another user backtest naturally occupied the single runner; it was not stopped or modified"
|
||||
}
|
||||
},
|
||||
"controls": {
|
||||
"currentClose": {
|
||||
"runIds": [
|
||||
"btr_1788775434926_632665_6",
|
||||
"btr_1788775464828_632665_7",
|
||||
"btr_1788775491657_632665_8"
|
||||
],
|
||||
"medianTotalSeconds": 10.044,
|
||||
"totalReturn": 0.3201517861398,
|
||||
"tradeCount": 5351,
|
||||
"canonicalSha256": "7204c6f41b8e39fbf1af7fc55cd601b80f3427a7aa058394ccd8b0b14ca48eed",
|
||||
"resultStoreSha256": "0b4d24ed5ec2b27cc4135707b4c51c78eb2c3e35a20da8108610778c30c72c73"
|
||||
},
|
||||
"staticDaily": {
|
||||
"runId": "btr_1788775531308_632665_9",
|
||||
"totalReturn": 0.13228843240310018,
|
||||
"tradeCount": 4445,
|
||||
"canonicalSha256": "fdfa855295c0b55bdbe6f39952ead1515e844bf033ced974d3c3ddc037a5d0b1",
|
||||
"resultStoreSha256": "697566645116c76ff837cd36f7f9bbd7ad3eb30510a5b95012fb730d5072d511"
|
||||
},
|
||||
"fullMinute": {
|
||||
"runId": "btr_1788775545217_632665_10",
|
||||
"totalReturn": 0.03473222656500008,
|
||||
"tradeCount": 156,
|
||||
"canonicalSha256": "7dae3a618932b90d36e9968c027f08a94d69b8fc6a56b1ea0cc1a2cb771d85b8",
|
||||
"resultStoreSha256": "bbbd7080b8fd7f6e8c3a8132499842bd0e4d644dbfdc3e8dbca7d2c0c1381e93"
|
||||
},
|
||||
"nextOpenFiveYear": {
|
||||
"totalReturn": 0.9922618879291,
|
||||
"tradeCount": 25827,
|
||||
"canonicalSha256": "ac1d167cb1e1073e1d1ecb01e914f94d7560081c1d238e6b4418d86233250719",
|
||||
"resultStoreSha256": "79570e0ae6b07badc1b693dc897dd1381647d259a4fe44ed3e50bc215e2fd088"
|
||||
},
|
||||
"terminalAuditsClean": true
|
||||
},
|
||||
"tradingRuntimeDeployment": {
|
||||
"sourceCommit": "bec62d0f7f6126a36ec0a101f7b1a66b138537eb",
|
||||
"strategyRuntimeBinarySha256": "b28aa9a20bff232d5d24ba6c8072c02ff921dea69a5ca3cb81712323e2d88d24",
|
||||
"healthPorts": [9100, 9101, 9102, 9103, 9104, 9130],
|
||||
"allHealthChecksPassed": true,
|
||||
"orderRoutingConfigurationChanged": false,
|
||||
"observedOrderRoutingMode": "disabled"
|
||||
},
|
||||
"decision": {
|
||||
"status": "accepted",
|
||||
"reason": "all business hashes and runtime rolling semantics remain exact while five-year DataSet construction, data time and steady memory-cold wall time improve",
|
||||
"nextTarget": "construct the immutable Base Panel directly from Arrow column buffers so the remaining SourceRowRecord and DailyFactorSnapshot row materialization can be removed"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"schemaVersion": "fidc-typed-current-rolling-helper/v1",
|
||||
"measuredAt": "2026-09-05T02:51:00+08:00",
|
||||
"host": "192.168.31.177",
|
||||
"engineCommit": "75ab0c06c66761d7b2b0edb3359359e13d1265e3",
|
||||
"serviceCommit": "9fd5a9e6d5668af57f6942fc3c4127953545d9c6",
|
||||
"runnerBinarySha256": "1071852a374620029398a967d485b827ed2defbb6ecbcc3f1ee6ffd3e2db76b6",
|
||||
"implementation": {
|
||||
"description": "compile static stock close/volume rolling_mean_current calls to typed DataSet kernels and bypass generic string normalization and helper dispatch",
|
||||
"evaluationRemainsLazy": true,
|
||||
"dynamicFieldsUseGenericFallback": true,
|
||||
"stockStateSizeChanged": false,
|
||||
"rollingFormulaChanged": false,
|
||||
"pitSemanticsChanged": false
|
||||
},
|
||||
"primaryFiveYearContract": {
|
||||
"startDate": "2021-08-23",
|
||||
"endDate": "2026-08-28",
|
||||
"frequency": "1d",
|
||||
"matchingType": "next_bar_open",
|
||||
"totalReturn": 0.9219861819172002,
|
||||
"tradeCount": 26088,
|
||||
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
|
||||
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
|
||||
"processCold": {
|
||||
"totalSeconds": 21.504,
|
||||
"dataSeconds": 12.753,
|
||||
"engineSeconds": 4.618,
|
||||
"unattributedSeconds": 3.489
|
||||
},
|
||||
"processHotEngineSeconds": [5.292, 4.729, 4.778, 4.635, 4.685],
|
||||
"processHotMedianEngineSeconds": 4.729,
|
||||
"calendarMajorBaselineMedianEngineSeconds": 5.202,
|
||||
"observedMedianImprovementPercent": 9.09265667051134,
|
||||
"resultConsistent": true,
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"secondaryFiveYearContract": {
|
||||
"totalReturn": 1.1342962298106998,
|
||||
"tradeCount": 19404,
|
||||
"canonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
|
||||
"resultStoreSha256": "416d2f87241fb4c6b917f6aeecb588f82f6e7d51a103f4c53a74d11971f16839",
|
||||
"engineSeconds": [4.031, 4.08, 4.049, 3.976],
|
||||
"medianEngineSeconds": 4.049,
|
||||
"calendarMajorBaselineMedianEngineSeconds": 4.878,
|
||||
"observedMedianImprovementPercent": 16.99466994669946,
|
||||
"resultConsistent": true,
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"hardwareCounters": {
|
||||
"candidateRunId": "btr_1788547776300_3162323_11",
|
||||
"candidateEngineSeconds": 4.782,
|
||||
"candidateCycles": 13368504015,
|
||||
"candidateInstructions": 26049740736,
|
||||
"candidateBranches": 4601262679,
|
||||
"candidateBranchMisses": 19416727,
|
||||
"fieldProjectionBaselineInstructions": 26663937176,
|
||||
"fieldProjectionBaselineBranches": 4751846020,
|
||||
"observedInstructionReductionPercent": 2.303472423993083,
|
||||
"observedBranchReductionPercent": 3.1689440349331863
|
||||
},
|
||||
"profile": {
|
||||
"runId": "btr_1788547704257_3162323_10",
|
||||
"engineSeconds": 12.057,
|
||||
"hostLoadAverageAfterRun": 35.31,
|
||||
"performanceComparisonExcluded": true,
|
||||
"genericResolveCurrentRollingMeanPresentInTopProfile": false,
|
||||
"typedCurrentVolumeKernelPercent": 2.25,
|
||||
"note": "profile percentages remain useful for call-path attribution, but this run overlapped heavy external and managed factor CPU load and is excluded from wall-time comparison"
|
||||
},
|
||||
"memory": {
|
||||
"serviceCgroupCurrentBytes": 11495362560,
|
||||
"serviceCgroupPeakBytes": 11497156608,
|
||||
"processRssKiB": 11213936,
|
||||
"processAnonymousKiB": 11197740
|
||||
},
|
||||
"testGate": {
|
||||
"coreUnitTotal": 421,
|
||||
"corePassed": 415,
|
||||
"ignoredManualBenchmarks": 6,
|
||||
"failed": 0,
|
||||
"integrationSuitesPassed": true
|
||||
},
|
||||
"remoteArtifacts": [
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/typed-current-rolling-primary-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/typed-current-rolling-secondary-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/typed-current-rolling-profile-20260905/perf.data",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/typed-current-rolling-profile-20260905/perf-report.txt",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/typed-current-rolling-perf-stat-20260905/perf-stat.csv"
|
||||
],
|
||||
"acceptance": {
|
||||
"status": "accepted_generic_typed_lazy_helper",
|
||||
"reason": "two different five-year contracts preserve exact outputs, both stable medians improve, hardware work falls, and the implementation keeps lazy short-circuit evaluation without enlarging per-stock state"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"schemaVersion": "fidc-uncached-selection-state/v1",
|
||||
"measuredAt": "2026-09-05T05:07:00+08:00",
|
||||
"host": "192.168.31.177",
|
||||
"engineCommit": "29faf7932ed7838d0a2178a34b3fe6a259bd9052",
|
||||
"serviceCommit": "9fd5a9e6d5668af57f6942fc3c4127953545d9c6",
|
||||
"runnerBinarySha256": "c67dbb4a4432697853a9146790ae507f1bc774506104a9be814b3435665e6c36",
|
||||
"implementation": {
|
||||
"description": "construct transient market-cap ordered selection states by value and leave rejected candidates out of the per-day Arc HashMap cache",
|
||||
"selectedStateBehavior": "later business use rebuilds and caches the selected or held symbol through the unchanged state API",
|
||||
"genericRankingChanged": false,
|
||||
"selectionResultCached": false,
|
||||
"rollingValueCached": false,
|
||||
"stateFieldsChanged": false,
|
||||
"pitSemanticsChanged": false
|
||||
},
|
||||
"primaryFiveYearContract": {
|
||||
"totalReturn": 0.9219861819172002,
|
||||
"tradeCount": 26088,
|
||||
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
|
||||
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
|
||||
"restartOrCold": {
|
||||
"totalSeconds": 16.142,
|
||||
"dataSeconds": 12.836,
|
||||
"engineSeconds": 2.665
|
||||
},
|
||||
"processHotEngineSeconds": [2.622, 2.683, 2.67, 2.619, 2.667],
|
||||
"processHotMedianEngineSeconds": 2.667,
|
||||
"previousAcceptedMedianEngineSeconds": 3.294,
|
||||
"observedMedianImprovementPercent": 19.034608,
|
||||
"resultConsistent": true,
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"secondaryFiveYearContract": {
|
||||
"totalReturn": 1.1342962298106998,
|
||||
"tradeCount": 19404,
|
||||
"canonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
|
||||
"resultStoreSha256": "416d2f87241fb4c6b917f6aeecb588f82f6e7d51a103f4c53a74d11971f16839",
|
||||
"engineSeconds": [2.318, 2.358, 2.35, 2.345, 2.367, 2.371],
|
||||
"medianEngineSeconds": 2.358,
|
||||
"previousAcceptedMedianEngineSeconds": 2.922,
|
||||
"observedMedianImprovementPercent": 19.301848,
|
||||
"resultConsistent": true,
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"fullMinuteContract": {
|
||||
"startDate": "2025-01-02",
|
||||
"endDate": "2025-11-17",
|
||||
"scheduleTime": "10:18",
|
||||
"matchingType": "minute_last",
|
||||
"slippageModel": "price_ratio",
|
||||
"slippageValue": 0.001,
|
||||
"totalReturn": 0.03473222656500008,
|
||||
"tradeCount": 156,
|
||||
"canonicalSha256": "457c086b1bca784fe83f447bb22451925e8f022456bb0fc7b54237a7a7886849",
|
||||
"resultStoreSha256": "04d804ee02ad7bc9b3649b4a2b4ddd902a4242ef38e00edeed8058860e9a6997",
|
||||
"totalSeconds": [0.592, 0.593],
|
||||
"engineSeconds": [0.162, 0.165],
|
||||
"resultConsistent": true,
|
||||
"terminalAuditStatus": "clean"
|
||||
},
|
||||
"profile": {
|
||||
"runId": "btr_1788555931028_369369_14",
|
||||
"engineSeconds": 2.672,
|
||||
"allThreadEventCycles": 62382573448,
|
||||
"previousAllThreadEventCycles": 77490041805,
|
||||
"observedCycleReductionPercent": 19.496013,
|
||||
"stockStateBuilderPercent": 12.33,
|
||||
"numericVmEvaluatePercent": 11.55,
|
||||
"adjustedCloseMovingAveragePercent": 5.32,
|
||||
"mallocPercent": 1.51,
|
||||
"previousMallocPercent": 2.82,
|
||||
"stockStateCacheClearInTopProfile": false,
|
||||
"stockStateDropGlueInTopProfile": false,
|
||||
"lostSamples": 0
|
||||
},
|
||||
"memory": {
|
||||
"serviceCgroupCurrentBytes": 11482611712,
|
||||
"serviceCgroupPeakBytes": 11484585984,
|
||||
"cacheMemoryAddedBytes": 0
|
||||
},
|
||||
"testGate": {
|
||||
"workspaceTotal": 544,
|
||||
"passed": 536,
|
||||
"ignoredManualBenchmarks": 8,
|
||||
"failed": 0,
|
||||
"strictClippyStatus": "baseline_blocked_by_137_preexisting_warnings"
|
||||
},
|
||||
"remoteArtifacts": [
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/uncached-selection-state-primary-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/uncached-selection-state-secondary-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/uncached-selection-state-full-minute-20260905.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/uncached-selection-state-profile-20260905/run.json",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/uncached-selection-state-profile-20260905/perf.data",
|
||||
"/srv/fidc/canonical/run/fidc-private/evidence/uncached-selection-state-profile-20260905/perf-report.txt"
|
||||
],
|
||||
"acceptance": {
|
||||
"status": "accepted_transient_selection_state",
|
||||
"reason": "two independent five-year contracts and the full-minute contract preserve exact outputs, both daily strategies reduce stable engine medians by about nineteen percent, all-thread cycles and allocator share fall, and no cache memory is added"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
# Factor Decision Phases
|
||||
|
||||
Status: broker foundation implemented; factor compiler, evaluator and runtime-plan integration are not complete. Do not advertise this as a fully working stock-pool buy-condition feature.
|
||||
|
||||
## Separate Contracts
|
||||
|
||||
| Phase | Meaning | Must Not Do |
|
||||
|---|---|---|
|
||||
| Selection | Build and rank the candidate universe at the strategy decision clock | Pretend this also guards every later top-up |
|
||||
| Buy permission | Decide whether this decision may create new buy exposure for a symbol | Convert a denied buy into a sell or silently drop a holding from a full target snapshot |
|
||||
| Exit/reduction | Produce the explicitly configured exit or partial target | Normalize remaining targets upward without an explicit strategy rule |
|
||||
| Execution risk | Apply actual execution-date price, ST, suspension, lifecycle, liquidity and cost constraints | Substitute decision-date risk facts for next-open execution facts |
|
||||
| Existing orders | Continue the already submitted order under its execution risk and lifetime contract | Implicitly cancel or rewrite it merely because a later decision has a new buy denial |
|
||||
|
||||
## Broker Primitive
|
||||
|
||||
`StrategyDecision.buy_denials` is a symbol-to-reason map sampled by the strategy layer, not a factor evaluator. Merged decisions retain denials. The broker installs it only while processing that decision and restores the prior context afterward; it is never shared through DataSet caches.
|
||||
|
||||
New positive buy quantities and target-buy planning respect the map after standard market/risk checks. Sells remain permitted. The actual execution price determines whether a value/portfolio target requires buying: a target below the signal-day holding value can become a buy after a lower next open, so signal-day direction alone is insufficient.
|
||||
|
||||
Existing resting orders are not automatically canceled by this primitive. A buy amendment is denied if it increases total quantity or raises the limit price, even if the other dimension decreases. Reductions in both dimensions remain allowed after normal validation. A rejected amendment emits an update-rejection process event without replacing the original order state or queue priority. Full runtime-plan integration still requires testing.
|
||||
|
||||
## Required Integration
|
||||
|
||||
1. Split selection and buy-role output in the stock-pool compiler instead of folding both into `stock_filter`.
|
||||
2. Evaluate buy expressions at the declared decision clock using typed field availability, units and frozen data identity. Missing data must retain its own diagnostic, not silently become a false trading signal.
|
||||
3. Populate denials for every symbol a decision can buy, including portfolio targets, retained-target reentry and top-ups. Do not infer execution direction from signal-day value.
|
||||
4. Preserve/consume constraints in Paper/Live strategy-plan conversion. No consumer may silently discard a nonempty denial map.
|
||||
5. Carry the tested amendment policy through runtime-plan conversion; validate source-date and execution-date risk independently.
|
||||
6. Verify same-bundle baseline parity when no buy constraint is configured, then test explicit buy failures across share, value, target and algorithmic orders.
|
||||
|
||||
## Current Evidence
|
||||
|
||||
On 177, broker tests verify blocked target top-ups, permitted sells, context restoration, existing pending-order preservation, a next-open target direction flip, and risk-increasing/reducing amendments with unchanged state on rejection. Full `fidc-core` tests passed: 453 unit tests and 122 integration tests, with 8 manual benchmarks ignored. The backtest runner previously compiled against the changed API.
|
||||
|
||||
The candidate is not deployed. The current OmniQuant compiler still needs the above integration, and no production readiness claim follows from these low-level tests.
|
||||
@@ -0,0 +1,75 @@
|
||||
# Fixed-Point and Minute-Stream Acceptance
|
||||
|
||||
Acceptance date: 2026-08-31
|
||||
Engine commit: `cd116bc3ae77cac0989eb80185bb04d7440b8834`
|
||||
|
||||
## Scope
|
||||
|
||||
This acceptance separates execution precision from minute-data throughput. It
|
||||
does not use a strategy-specific shortcut and does not change strategy,
|
||||
matching, risk, slippage, commission, tax, or future-data semantics.
|
||||
|
||||
## Fixed-Point Boundary
|
||||
|
||||
- Execution money is signed micro-yuan `i128`.
|
||||
- Stock gross amount, commission, stamp tax, transfer fee, cash, liabilities,
|
||||
external cash flow, account units, position lot cost and PnL are fixed-point.
|
||||
- Futures cash, margin, transaction cost, realized PnL and position PnL are
|
||||
fixed-point.
|
||||
- Market indicators and return statistics remain `f64`; conversion occurs only
|
||||
at the execution-money boundary.
|
||||
- One-micro-yuan budget overruns fail instead of being hidden by float epsilon.
|
||||
|
||||
Verification command:
|
||||
|
||||
```bash
|
||||
cargo test -p fidc-core fixed -- --nocapture
|
||||
```
|
||||
|
||||
Result: 8 passed, 0 failed.
|
||||
|
||||
## Minute Data Boundary
|
||||
|
||||
- `history_intraday_quotes_at` uses a sorted execution-date index and scans
|
||||
backward until the requested bar count is satisfied.
|
||||
- The active timestamp and `include_now` flag control visibility; later bars are
|
||||
never visible.
|
||||
- Daily minute execution consumes a borrowed k-way merge ordered by timestamp
|
||||
and symbol. It does not clone the complete selected quote day before engine
|
||||
dispatch.
|
||||
- Execution quotes are released by trading date after the day finishes.
|
||||
|
||||
Release benchmark command:
|
||||
|
||||
```bash
|
||||
cargo test -p fidc-core --test intraday_history_performance --release -- --ignored --nocapture
|
||||
```
|
||||
|
||||
Observed on the local acceptance host:
|
||||
|
||||
| Case | Workload | Result |
|
||||
| --- | --- | --- |
|
||||
| Bounded history | 200 queries over 60,000 rows | 0.000227 seconds, checksum 351450348000 |
|
||||
| Full-day materialization | 5,000 iterations x 240 rows | 0.049361 seconds |
|
||||
| Borrowed quote stream | 5,000 iterations x 240 rows | 0.012232 seconds |
|
||||
|
||||
The materialized and streamed timestamp checksums were both
|
||||
`2108693484000000`. The observed component speedup was about 4.04x. These
|
||||
numbers are component evidence only and are not an end-to-end SLA.
|
||||
|
||||
## Regression
|
||||
|
||||
```bash
|
||||
cargo test -p fidc-core --all-targets
|
||||
```
|
||||
|
||||
Result: 528 passed, 0 failed, 1 ignored manual benchmark. This includes
|
||||
execution-day risk, next-open open-price limits, minute timestamp visibility,
|
||||
slippage, minimum commission, stamp tax, volume limits, corporate actions,
|
||||
external cash-flow NAV treatment and futures account precision.
|
||||
|
||||
## Deployment Gate
|
||||
|
||||
This documentation-only correction does not require a service restart. Any
|
||||
future Source Lake or engine deployment still requires the official managed
|
||||
entrypoint and must fail closed while FIDC-managed factor work is active.
|
||||
@@ -0,0 +1,51 @@
|
||||
# Market Day View Component Benchmark
|
||||
|
||||
Date: 2026-08-31
|
||||
|
||||
## Scope
|
||||
|
||||
The platform-expression selection loop already iterates one factor slice for a
|
||||
single trading date. The previous implementation still resolved the same date
|
||||
in the market and candidate `BTreeMap` for every symbol. `DailySnapshotView`
|
||||
borrows the existing immutable market/factor/candidate slices and dense row
|
||||
position arrays once per date, then performs only `symbol_id -> row` lookups.
|
||||
|
||||
The view does not copy snapshots, cache strategy results, share account state,
|
||||
or change missing-row behavior. The optimization is independent of strategy
|
||||
text, thresholds, rolling windows, execution mode and portfolio size.
|
||||
|
||||
## Release Component A/B
|
||||
|
||||
Contract:
|
||||
|
||||
- 6,000 symbols;
|
||||
- 200 complete lookup rounds;
|
||||
- each lookup reads market close and candidate `allow_buy`;
|
||||
- baseline and view checksums must be exactly equal;
|
||||
- `cargo test --release`, system allocator, local macOS host.
|
||||
|
||||
| Round | Baseline seconds | Day view seconds |
|
||||
| ---: | ---: | ---: |
|
||||
| 1 | 0.009000 | 0.002939 |
|
||||
| 2 | 0.004370 | 0.001555 |
|
||||
| 3 | 0.004274 | 0.001578 |
|
||||
|
||||
Median component time changed from `0.004370s` to `0.001578s`, an observed
|
||||
reduction of about `63.9%` (`2.77x`). This is a component result only and is
|
||||
not a complete backtest SLA.
|
||||
|
||||
## Correctness Gates
|
||||
|
||||
- sparse market-only symbols remain absent from factor/candidate views;
|
||||
- dense and binary-search fallback lookup semantics remain unchanged;
|
||||
- full engine suite: 529 passed, 3 ignored manual benchmarks;
|
||||
- next-open execution-day risk, minute matching, fees, slippage, volume limits,
|
||||
corporate actions, delisting and futures tests all passed.
|
||||
|
||||
## Deployment Status
|
||||
|
||||
Not deployed. The 177 FIDC-managed Boris factor task is still active, so no
|
||||
Source Lake, backtest service or engine restart is allowed. After the task
|
||||
ends naturally, acceptance must use the same frozen bundle and compare daily
|
||||
selection, orders, fills, holdings, NAV, risk facts and canonical digest for
|
||||
multiple daily/minute and fixed/dynamic-universe strategies.
|
||||
@@ -0,0 +1,133 @@
|
||||
# FIDC 开源回测与交易引擎设计审查
|
||||
|
||||
## 审查范围
|
||||
|
||||
本次审查直接读取以下只读参考源码。源码位于
|
||||
`/Volumes/T7-Data/WorkSpace/reference-trading`;原计划使用的
|
||||
`/Volumes/SystemSSD` 在审查时未挂载,因此没有向本机系统盘写入参考仓库。
|
||||
|
||||
| 项目 | 审查提交 | 重点 |
|
||||
|---|---|---|
|
||||
| NautilusTrader | `ac22d5cf4a7e` | Rust 事件内核、统一回测/实盘组件、时间事件堆 |
|
||||
| QuantConnect LEAN | `23b735d99a35` | 订阅同步、TimeSlice、Universe 生命周期、惰性集合 |
|
||||
| Microsoft Qlib | `79633dd9506e` | 表达式缓存、日历切片、内存/磁盘分层 |
|
||||
| vectorbt | `34b6d5935e3e` | NumPy/Numba 密集数组仿真、紧凑状态数组 |
|
||||
| Zipline Reloaded | `943010b9da84` | Pipeline DAG、窗口预取、分段执行、复权读取 |
|
||||
| Backtrader | `b853d7c90b67` | preload/runonce 与逐 bar/live 模式分离 |
|
||||
|
||||
## 可借鉴设计
|
||||
|
||||
### 1. 时间轴和执行状态必须统一,但热路径不必经过通用消息总线
|
||||
|
||||
NautilusTrader 的 `BacktestEngine` 复用数据、执行、风险和缓存组件,并用带稳定
|
||||
序号的最小时间事件堆推进多个时钟。LEAN 用 `SubscriptionSynchronizer` 将不同订阅
|
||||
合并到同一个 frontier,再生成唯一 `TimeSlice`。两者共同证明:回测和实盘应共享
|
||||
订单、风控和时间语义,而不是共享一段策略特例代码。
|
||||
|
||||
FIDC 已经以 `decision_date`、`execution_date`、调度时间和撮合时间构成统一执行合同,
|
||||
并由同一交易核心服务于回测、模拟盘和实盘。日线全市场选股不应改为逐字段消息
|
||||
广播;这会给每个股票状态增加分配和动态分派。通用事件总线只保留在订单、成交、
|
||||
配置审计及外部集成边界。
|
||||
|
||||
### 2. 批量研究计算和事件撮合必须使用不同执行形态
|
||||
|
||||
vectorbt 把纯数值组合压入连续数组和编译循环;Backtrader 在历史批量模式使用
|
||||
`preload + runonce`,进入 live/replay 或受限内存模式后关闭该路径。Zipline 的
|
||||
Pipeline 则先生成 DAG 执行计划,按依赖顺序计算,并允许按日期 chunk 控制内存。
|
||||
|
||||
FIDC 应继续保持:
|
||||
|
||||
- 日线基础特征、rolling、横截面 rank 和因子挖掘使用 Arrow/NumPy/DuckDB/紧凑数组;
|
||||
- 订单、成交、T+1、涨跌停、停牌、公司行动和现金流使用确定性事件撮合;
|
||||
- 不把向量化收益外推到存在订单状态和路径依赖的撮合过程;
|
||||
- 不让逐 bar 实盘语义退化成预先知道整个未来数组的批量回测语义。
|
||||
|
||||
### 3. 不可变数据按内容身份共享,策略结果和可变账户状态严格隔离
|
||||
|
||||
Qlib 的缓存层、Zipline 的预取窗口和 Backtrader 的优化数据预载都说明:相同历史
|
||||
数据不应由每个策略重复解码。FIDC 当前 Source Lake 的 Parquet/Arrow、冻结 query
|
||||
scope、内容寻址 bundle、进程内 `DataSet` 和共享 result-store block 已符合这个方向。
|
||||
|
||||
共享键必须包含完整数据代际、字段投影、PIT 截止时间、复权口径和窗口。禁止共享:
|
||||
|
||||
- 选股结果、订单、仓位、账户、风控决策和策略局部变量;
|
||||
- 缺少 manifest/SHA/PIT 身份的 DataFrame 或 dict;
|
||||
- 盘中 provisional 数据与正式收盘数据混用的缓存项。
|
||||
|
||||
### 4. 字段和因子要在计划阶段冻结,运行时只物化真正需要的数据
|
||||
|
||||
LEAN 的 `TimeSliceFactory` 复用空集合,并只在收到对应数据时创建集合;Zipline 的
|
||||
Pipeline 使用执行计划和 refcount 释放中间值。FIDC 已有字段投影、
|
||||
`DailySnapshotView`、rolling requirement、Factor DAG 和 numeric bytecode VM。
|
||||
后续优化必须扩展这些类型化计划,而不是恢复宽 Python 行或每次构建完整 map。
|
||||
|
||||
## 当前性能事实
|
||||
|
||||
2026-09-06 在 177 使用同一冻结五年策略得到:
|
||||
|
||||
- restart/cold:`17.985s`;数据准备 `14.659s`;引擎 `2.627s`;
|
||||
- process-hot:总耗时中位 `3.245s`;数据准备 `0.006s`;引擎 `2.629s`;
|
||||
- 26,088 笔成交、收益 `0.9219861819172002`、canonical SHA
|
||||
`b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234`
|
||||
在所有重复运行中一致;
|
||||
- 当前剖面热点为 numeric VM `12.39%`、临时股票状态构建 `10.17%`、复权均线
|
||||
`6.82%`;
|
||||
- Source Lake/进程共享缓存已经把同 bundle 数据准备从 `14.659s` 降到 `0.006s`,
|
||||
所以再把 DataFrame 改成 dict 或扩大 DuckDB 连接数不是当前热路径优化。
|
||||
|
||||
两项受开源类型化执行启发的候选已真实验证并删除:
|
||||
|
||||
1. 编译期枚举化全部 VM 标识符仅减少约 `1.44%` instructions,五年引擎中位只改善
|
||||
`0.19%`,却净增约 562 行;
|
||||
2. VM 槽位代际复用仅减少约 `0.48%` instructions,五年引擎中位回退约 `0.27%`。
|
||||
|
||||
完整证据见 `docs/evidence/numeric-vm-binding-generation-rejection-20260906.json`。
|
||||
|
||||
## FIDC 后续优化顺序
|
||||
|
||||
### P0:冷数据路径一次构建、分段发布、跨策略共享
|
||||
|
||||
以冻结 bundle 的完整身份构建 canonical base panel,并按年份或有界日期段发布
|
||||
只读 mmap/Arrow 段;父任务只扫描一次 Source Lake,worker 只映射所需段。每日增量
|
||||
只生成变化尾段,历史段保持同 SHA。必须以 process-cold `14.659s` 为基线,证明
|
||||
冷启动下降且 RSS、PIT、复权和结果 SHA 不变。
|
||||
|
||||
### P1:numeric VM 使用类型化 helper opcode 或经证据支持的 super-instruction
|
||||
|
||||
当前字符串标识符绑定不是主要成本。下一候选应在编译期把常见 rolling helper、
|
||||
比较和短路组合编译为类型化 opcode,减少解释器指令数,同时保留 helper 懒求值和
|
||||
错误信息。必须对主策略、第二种持仓数策略、通用自定义排序策略和分钟策略分别 A/B。
|
||||
|
||||
### P1:结果事件按消费者需求分层
|
||||
|
||||
撮合事实保持完整不可变;页面摘要、曲线、持仓和交易视图从 typed result store
|
||||
按需投影。禁止每次回测向 PostgreSQL 写入大矩阵,也禁止为了列表或概览解码全部
|
||||
事件。优化目标是 `resultSeconds + finalizationSeconds`,不能删减审计事实换速度。
|
||||
|
||||
### P2:分钟线有界流式窗口
|
||||
|
||||
参考 Zipline 的窗口预取和 Nautilus 的有序事件迭代器,按时间段加载分钟
|
||||
RecordBatch,保持持仓、订单和指标 ring buffer 有界;不能一次展开全市场全区间
|
||||
分钟 Python 对象,也不能在 chunk 边界丢失 corporate action、T+1 或订单队列状态。
|
||||
|
||||
## 明确不采用
|
||||
|
||||
- 不为单次策略把日循环拆成多线程;路径依赖会增加同步开销并破坏确定性。
|
||||
- 不通过增加 worker、DuckDB 槽位或扩大 HTTP 窗口掩盖单任务热点。
|
||||
- 不把全量历史数据复制成每个 worker 独占的 dict/DataFrame 缓存。
|
||||
- 不直接引入另一个框架的事件总线、账户或撮合实现;只借鉴机制并用中国市场合同验收。
|
||||
- 不缓存策略结果,也不以 request hash 返回旧结果代替真实回测。
|
||||
|
||||
## 验收矩阵
|
||||
|
||||
任何性能候选至少覆盖:
|
||||
|
||||
| 合同 | 必须保持 |
|
||||
|---|---|
|
||||
| 五年主策略 | 收益、成交数、canonical/result-store SHA、终态审计 |
|
||||
| 五年第二策略 | 不同持仓数下的同一组证据 |
|
||||
| 通用 rank 策略 | 非 market-cap 特例排序仍正确 |
|
||||
| 分钟策略 | 调度点、分钟成交价、滑点、成交量限制和 SHA |
|
||||
| 冷/热运行 | data/engine/result/finalization 分段、RSS、instructions/cycles |
|
||||
|
||||
任一合同漂移、只有微基准改善、或真实 wall/RSS 变差时,候选必须删除并保留拒绝证据。
|
||||
Executable
+164
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
fail() {
|
||||
local message="$1"
|
||||
local details="${2:-}"
|
||||
printf '[FAIL] %s\n' "$message" >&2
|
||||
if [[ -n "$details" ]]; then
|
||||
printf '%s\n' "$details" >&2
|
||||
fi
|
||||
exit 1
|
||||
}
|
||||
|
||||
runtime_hits="$(
|
||||
rg -n \
|
||||
--glob '!**/.git/**' \
|
||||
--glob '!**/target/**' \
|
||||
--glob '!**/docs/**' \
|
||||
--glob '!**/*.md' \
|
||||
--glob '!**/tests/**' \
|
||||
--glob '!**/*.test.rs' \
|
||||
--glob '!**/*_test.rs' \
|
||||
--glob '!crates/fidc-core/src/strategy_ai.rs' \
|
||||
--glob '!scripts/verify-no-legacy-data-source.sh' \
|
||||
'fidatacenter|FIDATACENTER|/v1/backtest/data|/v1/xuntou|ClickHouse|clickhouse|CLICKHOUSE|FIDC_BT_INSTRUMENT_METADATA_CSV|FIDC_BT_WRITE_SNAPSHOTS|FIDC_BT_WRITE_CSV_SNAPSHOTS|FIDC_BT_WRITE_COMBINED_SOURCE_ROW_CACHE|write_csv_snapshot_files|write_csv\(|FIDC_RISK_RUNTIME_FILE|FIDC_RISK_RUNTIME_URL|FIDC_FIRISK_RUNTIME_FILE|FiRisk runtime snapshot' \
|
||||
crates Cargo.toml \
|
||||
2>/dev/null || true
|
||||
)"
|
||||
|
||||
if [[ -n "$runtime_hits" ]]; then
|
||||
fail "legacy fidatacenter/ClickHouse/CSV snapshot/FiRisk runtime data source references are not allowed in fidc-backtest-engine" "$runtime_hits"
|
||||
fi
|
||||
|
||||
manifest_hits="$(
|
||||
rg -n --glob '!scripts/verify-no-legacy-data-source.sh' '\b(mysql|mariadb|clickhouse|clickhouse-rs|mysql_async|sqlx-mysql)\b' Cargo.toml crates 2>/dev/null || true
|
||||
)"
|
||||
|
||||
if [[ -n "$manifest_hits" ]]; then
|
||||
fail "legacy database dependencies are not allowed in fidc-backtest-engine manifests" "$manifest_hits"
|
||||
fi
|
||||
|
||||
local_only_hits="$(
|
||||
rg -n \
|
||||
--glob '!**/.git/**' \
|
||||
--glob '!**/target/**' \
|
||||
--glob '!**/docs/**' \
|
||||
--glob '!**/*.md' \
|
||||
--glob '!**/tests/**' \
|
||||
--glob '!**/*.test.rs' \
|
||||
--glob '!**/*_test.rs' \
|
||||
--glob '!crates/fidc-core/src/strategy_ai.rs' \
|
||||
--glob '!scripts/verify-no-legacy-data-source.sh' \
|
||||
'FICLAW_DATA_AGENT_URL|ficlaw_data\.source_rows_v1|strategy-factory-source-lake://local|FIDC_BT_WRITE_COMBINED_SOURCE_ROW_CACHE' \
|
||||
crates Cargo.toml \
|
||||
2>/dev/null || true
|
||||
)"
|
||||
|
||||
if [[ -n "$local_only_hits" ]]; then
|
||||
fail "legacy or local-only data source references are not allowed in fidc-backtest-engine runtime" "$local_only_hits"
|
||||
fi
|
||||
|
||||
json_query_hits="$(
|
||||
rg -n \
|
||||
--glob '!**/.git/**' \
|
||||
--glob '!**/target/**' \
|
||||
--glob '!**/docs/**' \
|
||||
--glob '!**/*.md' \
|
||||
--glob '!**/tests/**' \
|
||||
--glob '!**/*.test.rs' \
|
||||
--glob '!**/*_test.rs' \
|
||||
--glob '!crates/fidc-core/src/strategy_ai.rs' \
|
||||
--glob '!scripts/verify-no-legacy-data-source.sh' \
|
||||
'/v1/query/(source-rows|daily-execution-prices|minute-execution-prices|instruments|corporate-actions)\.json' \
|
||||
crates Cargo.toml \
|
||||
2>/dev/null || true
|
||||
)"
|
||||
|
||||
if [[ -n "$json_query_hits" ]]; then
|
||||
fail "JSON Source Lake high-throughput endpoints are not allowed in fidc-backtest-engine runtime; use Arrow endpoints" "$json_query_hits"
|
||||
fi
|
||||
|
||||
fused_hits="$(
|
||||
rg -n \
|
||||
--glob '!**/.git/**' \
|
||||
--glob '!**/target/**' \
|
||||
--glob '!**/docs/**' \
|
||||
--glob '!**/*.md' \
|
||||
--glob '!**/tests/**' \
|
||||
--glob '!**/*.test.rs' \
|
||||
--glob '!**/*_test.rs' \
|
||||
--glob '!crates/fidc-core/src/strategy_ai.rs' \
|
||||
--glob '!scripts/verify-no-legacy-data-source.sh' \
|
||||
'exported_fused|fidc_fused|fusion|fused|wide_table|wide table|source_rows_export|exported_daily|merged_daily|daily_merged|merged_source|materialized[_ -]source|materialized_source_rows|source_rows_materialized|融合表|融合宽表' \
|
||||
crates Cargo.toml \
|
||||
2>/dev/null || true
|
||||
)"
|
||||
|
||||
if [[ -n "$fused_hits" ]]; then
|
||||
fail "exported fused tables are not allowed in fidc-backtest-engine runtime; use Strategy Factory Source Lake source rows directly" "$fused_hits"
|
||||
fi
|
||||
|
||||
feature_store_hits="$(
|
||||
rg -n \
|
||||
--glob '!**/.git/**' \
|
||||
--glob '!**/target/**' \
|
||||
--glob '!**/docs/**' \
|
||||
--glob '!**/*.md' \
|
||||
--glob '!**/tests/**' \
|
||||
--glob '!**/*.test.rs' \
|
||||
--glob '!**/*_test.rs' \
|
||||
--glob '!crates/fidc-core/src/strategy_ai.rs' \
|
||||
--glob '!scripts/verify-no-legacy-data-source.sh' \
|
||||
'research_feature_store|feature_store|FEATURE_STORE|daily_minute_current|FIDC_STRATEGY_FACTORY_ENABLE_FEATURE_STORE_CACHE|ALPHA_FACTORY_ENABLE_FEATURE_STORE_CACHE|ENABLE_FEATURE_STORE_CACHE' \
|
||||
crates Cargo.toml \
|
||||
2>/dev/null || true
|
||||
)"
|
||||
|
||||
if [[ -n "$feature_store_hits" ]]; then
|
||||
fail "historical feature-store paths are not allowed in fidc-backtest-engine runtime; use Strategy Factory Source Lake raw/indicator/artifact partitions and discardable caches" "$feature_store_hits"
|
||||
fi
|
||||
|
||||
truth_csv_hits="$(
|
||||
rg -n \
|
||||
--glob '!**/.git/**' \
|
||||
--glob '!**/target/**' \
|
||||
--glob '!**/docs/**' \
|
||||
--glob '!**/*.md' \
|
||||
--glob '!**/tests/**' \
|
||||
--glob '!**/*.test.rs' \
|
||||
--glob '!**/*_test.rs' \
|
||||
--glob '!crates/fidc-core/src/strategy_ai.rs' \
|
||||
--glob '!scripts/verify-no-legacy-data-source.sh' \
|
||||
'FIDC_BT_TRUTH_STOCK_LIST_CSV|OMNI_BT_TRUTH_STOCK_LIST_CSV|OMNI_BACKTEST_TRUTH_STOCK_LIST_CSV|selection_source=truth_csv|truth_stock_list|truth_csv' \
|
||||
crates Cargo.toml \
|
||||
2>/dev/null || true
|
||||
)"
|
||||
|
||||
if [[ -n "$truth_csv_hits" ]]; then
|
||||
fail "CSV truth stock-list overrides are not allowed in fidc-backtest-engine runtime; use Source Lake runtime spec selection only" "$truth_csv_hits"
|
||||
fi
|
||||
|
||||
csv_snapshot_loader_hits="$(
|
||||
rg -n \
|
||||
--glob '!**/.git/**' \
|
||||
--glob '!**/target/**' \
|
||||
--glob '!**/docs/**' \
|
||||
--glob '!**/*.md' \
|
||||
--glob '!**/tests/**' \
|
||||
--glob '!**/*.test.rs' \
|
||||
--glob '!**/*_test.rs' \
|
||||
--glob '!scripts/verify-no-legacy-data-source.sh' \
|
||||
'from_csv_dir|from_partitioned_dir|instruments\.csv|candidate_flags\.csv|market\.csv|benchmark\.csv' \
|
||||
crates Cargo.toml \
|
||||
2>/dev/null || true
|
||||
)"
|
||||
|
||||
if [[ -n "$csv_snapshot_loader_hits" ]]; then
|
||||
fail "CSV snapshot loaders are not allowed in fidc-backtest-engine runtime; construct DataSet from Source Lake components" "$csv_snapshot_loader_hits"
|
||||
fi
|
||||
|
||||
printf '[OK] fidc-backtest-engine has no legacy runtime data-source references\n'
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
PYTHON_BIN="${PYTHON_BIN:-python3}"
|
||||
|
||||
fail() {
|
||||
local message="$1"
|
||||
local details="${2:-}"
|
||||
printf '[FAIL] %s\n' "$message" >&2
|
||||
if [[ -n "$details" ]]; then
|
||||
printf '%s\n' "$details" >&2
|
||||
fi
|
||||
exit 1
|
||||
}
|
||||
|
||||
run_core_test() {
|
||||
local filter="$1"
|
||||
local tmp
|
||||
tmp="$(mktemp)"
|
||||
printf '[INFO] cargo test -p fidc-core %s\n' "$filter"
|
||||
if cargo test -p fidc-core "$filter" -- --nocapture 2>&1 | tee "$tmp"; then
|
||||
local passed_count
|
||||
passed_count="$(
|
||||
"$PYTHON_BIN" - "$tmp" <<'PY'
|
||||
import re
|
||||
import sys
|
||||
|
||||
count = 0
|
||||
for line in open(sys.argv[1], encoding="utf-8", errors="replace"):
|
||||
match = re.search(r"test result: ok\. (\d+) passed;", line)
|
||||
if match:
|
||||
count += int(match.group(1))
|
||||
print(count)
|
||||
PY
|
||||
)"
|
||||
rm -f "$tmp"
|
||||
if [[ "$passed_count" -le 0 ]]; then
|
||||
fail "cargo test filter matched 0 tests: package=fidc-core filter=${filter}"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
local output
|
||||
output="$(cat "$tmp")"
|
||||
rm -f "$tmp"
|
||||
fail "cargo test failed: package=fidc-core filter=${filter}" "$output"
|
||||
}
|
||||
|
||||
run_core_test eligible_universe_does_not_require_candidate_risk_state_when_selection_risk_is_disabled
|
||||
run_core_test next_bar_open_eligible_universe_helper_does_not_block_on_decision_day_risk
|
||||
run_core_test platform_selection_uses_complete_static_risk_policy_before_universe_output
|
||||
run_core_test platform_next_open_defers_complete_static_selection_risk_before_universe_output
|
||||
run_core_test platform_next_open_selection_ignores_execution_day_limit_state
|
||||
run_core_test platform_next_open_selection_applies_explicit_signal_day_risk_filter
|
||||
run_core_test next_open_buy_risk_uses_execution_date_not_signal_date
|
||||
run_core_test next_open_buy_limit_risk_uses_open_not_close
|
||||
run_core_test next_open_sell_risk_uses_execution_date_not_signal_date
|
||||
run_core_test next_open_sell_limit_risk_uses_open_not_close
|
||||
run_core_test next_bar_open_sell_respects_allow_sell_policy_on_execution_day
|
||||
run_core_test volume_limit_uses_floor_for_odd_lot_sell
|
||||
run_core_test configurable_upper_limit_buy_filter_can_be_disabled
|
||||
|
||||
printf '[OK] fidc-backtest-engine runtime risk contracts passed\n'
|
||||
Reference in New Issue
Block a user