cfquant 0.2.24__tar.gz → 0.2.35__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cfquant-0.2.24 → cfquant-0.2.35}/PKG-INFO +3 -3
- {cfquant-0.2.24 → cfquant-0.2.35}/README.md +2 -2
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/_editable_install.py +19 -0
- cfquant-0.2.35/cfquant/log_management.py +97 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/normal_bridge.py +34 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/qmt_bridge.py +4 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/qmt_strategy_deploy.py +53 -8
- cfquant-0.2.35/cfquant/tests/32_/345/274/202/346/255/245/344/270/213/345/215/225/346/265/213/350/257/225.py +176 -0
- cfquant-0.2.35/cfquant/tests/33_/345/220/214/346/255/245/344/270/213/345/215/225/345/233/236/350/260/203/346/265/213/350/257/225.py +160 -0
- cfquant-0.2.35/cfquant/tests/log/cftrader/346/250/241/346/213/237/350/264/246/345/217/267/346/211/271/351/207/217/344/270/213/345/215/225/346/265/213/350/257/225_20260921_175732.md +12 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/test_api_tester_web.py +19 -0
- cfquant-0.2.35/cfquant/tests/test_log_management.py +32 -0
- cfquant-0.2.35/cfquant/tests/test_management_batch.py +55 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/test_qmt_strategy_deploy.py +17 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/test_qmt_strategy_web.py +8 -2
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tx.py +12 -4
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tx_trade_bridge.py +4 -0
- cfquant-0.2.35/cfquant/version.py +5 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/xttrader.py +77 -22
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/xttype.py +273 -15
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant.egg-info/PKG-INFO +3 -3
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant.egg-info/SOURCES.txt +6 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant_web_server.py +261 -70
- {cfquant-0.2.24 → cfquant-0.2.35}/pyproject.toml +1 -1
- {cfquant-0.2.24 → cfquant-0.2.35}/web_dashboard/app.js +476 -38
- {cfquant-0.2.24 → cfquant-0.2.35}/web_dashboard/index.html +31 -4
- {cfquant-0.2.24 → cfquant-0.2.35}/web_dashboard/python-api.js +3 -1
- {cfquant-0.2.24 → cfquant-0.2.35}/web_dashboard/styles.css +5597 -5562
- cfquant-0.2.24/cfquant/version.py +0 -5
- {cfquant-0.2.24 → cfquant-0.2.35}/LICENSE +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/LTtx/__init__.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/LTtx/tx/Config.txt +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/LTtx/tx/LTtx_server.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/LTtx/tx/__init__.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/LTtx/tx/tx.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/MANIFEST.in +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/__init__.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/account_routing.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/batch_orders.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/cftrader.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/channels.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/cli.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/client.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/config.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/hub.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/level2.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/logging_i18n.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/order_meta.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/pipe_bridge.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/pipe_client.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/pipe_hub.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/pipe_transport.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/protocol.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/qmt_strategy_package.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/qmt_strategy_runtime.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/runtime_report.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/10_/345/221/275/344/273/244/350/241/214/346/216/245/345/217/243/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/11_/346/231/256/351/200/232/346/241/245/346/216/245/346/264/276/345/217/221/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/12_/350/256/242/345/215/225/345/244/207/346/263/250/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/13_/347/256/241/351/201/223/344/270/255/345/277/203/350/277/236/346/216/245/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/14_/350/277/220/350/241/214/346/227/266/346/212/245/345/221/212/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/15_/347/275/221/351/241/265/347/212/266/346/200/201/347/274/223/345/255/230/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/16_/345/220/257/345/212/250/345/256/211/350/243/205/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/17_/345/244/247QMT/346/216/245/345/217/243/351/200/202/351/205/215/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/18_/351/253/230/347/272/247/346/250/241/345/274/217/345/256/236/346/234/272/350/201/224/350/260/203.py" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/19_/345/256/236/346/234/272/350/201/224/350/260/203/345/210/244/345/256/232/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/1_/350/241/214/346/203/205/346/216/245/346/224/266/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/20_/350/201/224/350/260/203/351/227/256/351/242/230/344/277/256/345/244/215/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/21_/345/215/225/350/202/241/350/241/214/346/203/205/350/256/242/351/230/205/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/22_/350/277/236/346/216/245/350/257/212/346/226/255/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/23_/347/244/272/344/276/213/351/205/215/347/275/256/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/24_/350/264/246/346/210/267/347/274/223/345/255/230/346/214/211/351/234/200/345/210/267/346/226/260/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/25_/350/264/246/346/210/267/347/274/223/345/255/230/351/241/265/351/235/242/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/26_/345/205/250/346/216/250/350/256/242/351/230/205/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/27_/345/205/250/346/216/250/350/256/242/351/230/205/351/241/265/351/235/242/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/28_Level2/346/216/245/345/217/243/351/200/202/351/205/215/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/29_Level2/350/241/214/346/203/205/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/2_/346/225/260/346/215/256/350/216/267/345/217/226/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/30_cftrader/346/211/271/351/207/217/344/270/213/345/215/225/346/200/247/350/203/275/345/237/272/345/207/206.py" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/31_cftrader/346/250/241/346/213/237/350/264/246/345/217/267/346/211/271/351/207/217/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/3_/346/225/260/346/215/256/344/270/213/350/275/275/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/4_/344/272/244/346/230/223/345/247/224/346/211/230/346/237/245/350/257/242/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/5_/347/234/237/345/256/236/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/6_/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/7_/345/220/214/346/255/245/345/274/202/346/255/245/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/8_/346/210/220/344/272/244/350/256/242/345/215/225/345/222/214/345/247/224/346/211/230/350/256/242/345/215/225/346/237/245/350/257/242/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/9_/345/217/257/346/222/244/350/256/242/345/215/225/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/_helpers.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/log/cftrader/346/250/241/346/213/237/350/264/246/345/217/267/346/211/271/351/207/217/344/270/213/345/215/225/346/265/213/350/257/225_20260911_024617.md" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/readme.md +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/test_api_tester_browser.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/test_cftrader.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/test_cftrader_tutorial.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/test_cftrader_web.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/test_credit_compat.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/test_json_serialization.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/test_project_updater.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/test_python_api_reference.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/test_qmt_strategy_browser.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/test_tutorial_reader.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/tests/test_tx_auto_import.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/xtconstant.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant/xtdata.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant.egg-info/dependency_links.txt +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant.egg-info/entry_points.txt +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant.egg-info/requires.txt +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant.egg-info/top_level.txt +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/cfquant_pipe_hub.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/qmt_scripts/CFQUANT.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/qmt_scripts/CFQUANT_LITE.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/qmt_scripts/__init__.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_CTYPE_ALL_LOWLAT_SH.py" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_CTYPE_ALL_LOWLAT_SZ.py" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_LITE_SH.py" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_LITE_SZ.py" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_TRADE_LOWLAT_SH.py" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_TRADE_LOWLAT_SZ.py" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/readme.md" +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/requirements.txt +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/setup.cfg +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/web_dashboard/__init__.py +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/web_dashboard/api-tester.js +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/web_dashboard/avatars/amber-pulse.svg +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/web_dashboard/avatars/copper-grid.svg +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/web_dashboard/avatars/market-blue.svg +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/web_dashboard/avatars/rose-circuit.svg +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/web_dashboard/avatars/signal-green.svg +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/web_dashboard/avatars/slate-wave.svg +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/web_dashboard/avatars/teal-orbit.svg +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/web_dashboard/avatars/violet-node.svg +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/web_dashboard/cftrader-api-data.js +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/web_dashboard/favicon.svg +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/web_dashboard/python-api-data.js +0 -0
- {cfquant-0.2.24 → cfquant-0.2.35}/web_dashboard/vendor/mermaid.min.js +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: cfquant
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.35
|
|
4
4
|
Summary: QMT local bridge and xtquant-compatible Python client.
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.8
|
|
@@ -123,7 +123,7 @@ cfquant run
|
|
|
123
123
|
- 如果新版本修改了 `qmt_scripts/` 里的入口脚本,网页会提示你重新更新 QMT 侧脚本并重启对应 QMT 策略。
|
|
124
124
|
- 建议把源码目录固定下来,例如 `D:\cfquant`,不要频繁挪动目录。
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
新用户建议先使用**通用模式**。所有部署配置都在 Web 控制台完成,保存绑定后系统会自动准备并管理 QMT 托管策略。
|
|
127
127
|
|
|
128
128
|
### Web 控制台绑定与初始化流程
|
|
129
129
|
|
|
@@ -209,7 +209,7 @@ restart_cfquant.bat 重启
|
|
|
209
209
|
|
|
210
210
|
|
|
211
211
|
## 项目交流群
|
|
212
|
-
<img src="
|
|
212
|
+
<img src="docs/f5f1d14256e57023dd67513481f16718.jpg" alt="cfquant 项目交流群二维码" width="280" />
|
|
213
213
|
|
|
214
214
|
|
|
215
215
|
## 联系作者
|
|
@@ -101,7 +101,7 @@ cfquant run
|
|
|
101
101
|
- 如果新版本修改了 `qmt_scripts/` 里的入口脚本,网页会提示你重新更新 QMT 侧脚本并重启对应 QMT 策略。
|
|
102
102
|
- 建议把源码目录固定下来,例如 `D:\cfquant`,不要频繁挪动目录。
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
新用户建议先使用**通用模式**。所有部署配置都在 Web 控制台完成,保存绑定后系统会自动准备并管理 QMT 托管策略。
|
|
105
105
|
|
|
106
106
|
### Web 控制台绑定与初始化流程
|
|
107
107
|
|
|
@@ -187,7 +187,7 @@ restart_cfquant.bat 重启
|
|
|
187
187
|
|
|
188
188
|
|
|
189
189
|
## 项目交流群
|
|
190
|
-
<img src="
|
|
190
|
+
<img src="docs/f5f1d14256e57023dd67513481f16718.jpg" alt="cfquant 项目交流群二维码" width="280" />
|
|
191
191
|
|
|
192
192
|
|
|
193
193
|
## 联系作者
|
|
@@ -11,6 +11,21 @@ from pathlib import Path
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
PACKAGE_NAME = "cfquant"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _hidden_subprocess_kwargs():
|
|
17
|
+
"""Prevent pip/version helper processes from flashing console windows on Windows."""
|
|
18
|
+
if os.name != "nt":
|
|
19
|
+
return {}
|
|
20
|
+
kwargs = {"creationflags": getattr(subprocess, "CREATE_NO_WINDOW", 0)}
|
|
21
|
+
try:
|
|
22
|
+
startupinfo = subprocess.STARTUPINFO()
|
|
23
|
+
startupinfo.dwFlags |= getattr(subprocess, "STARTF_USESHOWWINDOW", 1)
|
|
24
|
+
startupinfo.wShowWindow = getattr(subprocess, "SW_HIDE", 0)
|
|
25
|
+
kwargs["startupinfo"] = startupinfo
|
|
26
|
+
except Exception:
|
|
27
|
+
pass
|
|
28
|
+
return kwargs
|
|
14
29
|
DEFAULT_PIP_INDEX_URL = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
|
15
30
|
PIP_INDEX_URL_ENV = "CFQUANT_PIP_INDEX_URL"
|
|
16
31
|
INSTALLED_CHECK_CODE = (
|
|
@@ -122,6 +137,7 @@ def _run_pip_command(
|
|
|
122
137
|
"errors": "replace",
|
|
123
138
|
"timeout": float(timeout),
|
|
124
139
|
}
|
|
140
|
+
kwargs.update(_hidden_subprocess_kwargs())
|
|
125
141
|
if subprocess_kwargs:
|
|
126
142
|
kwargs.update(dict(subprocess_kwargs))
|
|
127
143
|
try:
|
|
@@ -326,6 +342,7 @@ def run_editable_install(
|
|
|
326
342
|
"errors": "replace",
|
|
327
343
|
"timeout": float(timeout),
|
|
328
344
|
}
|
|
345
|
+
kwargs.update(_hidden_subprocess_kwargs())
|
|
329
346
|
if subprocess_kwargs:
|
|
330
347
|
kwargs.update(dict(subprocess_kwargs))
|
|
331
348
|
try:
|
|
@@ -436,6 +453,7 @@ def run(args, cwd, quiet=False, clear_pythonpath=False):
|
|
|
436
453
|
else python_environment(clear_pythonpath=clear_pythonpath)),
|
|
437
454
|
stdout=stdout,
|
|
438
455
|
stderr=stderr,
|
|
456
|
+
**_hidden_subprocess_kwargs(),
|
|
439
457
|
)
|
|
440
458
|
|
|
441
459
|
|
|
@@ -468,6 +486,7 @@ def installed_package_version(python_exe=None):
|
|
|
468
486
|
env=python_environment(clear_pythonpath=True),
|
|
469
487
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
470
488
|
text=True, encoding="utf-8", timeout=30,
|
|
489
|
+
**_hidden_subprocess_kwargs(),
|
|
471
490
|
)
|
|
472
491
|
return completed.stdout.strip() if completed.returncode == 0 else ""
|
|
473
492
|
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Bounded local log browsing and rotation, independent of the web server."""
|
|
2
|
+
import datetime
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
import uuid
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def retention_days(value):
|
|
12
|
+
if isinstance(value, bool) or not re.fullmatch(r"[0-9]+", str(value)):
|
|
13
|
+
raise ValueError("retention_days must be an integer between 1 and 3650")
|
|
14
|
+
result = int(value)
|
|
15
|
+
if not 1 <= result <= 3650:
|
|
16
|
+
raise ValueError("retention_days must be between 1 and 3650")
|
|
17
|
+
return result
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def log_files(root, day):
|
|
21
|
+
datetime.date.fromisoformat(day)
|
|
22
|
+
root = Path(root).resolve()
|
|
23
|
+
rows = []
|
|
24
|
+
if not root.is_dir():
|
|
25
|
+
return rows
|
|
26
|
+
# The managed log directory only; never traverse symlinks or user paths.
|
|
27
|
+
for current, dirs, files in os.walk(root, followlinks=False):
|
|
28
|
+
dirs[:] = [d for d in dirs if not Path(current, d).is_symlink()]
|
|
29
|
+
for name in files:
|
|
30
|
+
path = Path(current, name)
|
|
31
|
+
if path.is_symlink() or path.suffix.lower() != '.log':
|
|
32
|
+
continue
|
|
33
|
+
try:
|
|
34
|
+
stat = path.stat()
|
|
35
|
+
except OSError:
|
|
36
|
+
continue
|
|
37
|
+
modified = datetime.datetime.fromtimestamp(stat.st_mtime).date().isoformat()
|
|
38
|
+
dates = re.findall(r"(?<!\d)(20\d{2})[-_]?(\d{2})[-_]?(\d{2})(?!\d)", name)
|
|
39
|
+
if modified != day and day not in ['-'.join(parts) for parts in dates]:
|
|
40
|
+
continue
|
|
41
|
+
rows.append({'name': path.relative_to(root).as_posix(), 'size': stat.st_size,
|
|
42
|
+
'modified': stat.st_mtime})
|
|
43
|
+
if len(rows) >= 1000:
|
|
44
|
+
return rows
|
|
45
|
+
return sorted(rows, key=lambda row: row['modified'], reverse=True)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def read_log(root, name, limit=256 * 1024):
|
|
49
|
+
root = Path(root).resolve()
|
|
50
|
+
path = (root / name).resolve()
|
|
51
|
+
if not name or root not in path.parents or path.suffix.lower() != '.log':
|
|
52
|
+
raise ValueError('Invalid log file')
|
|
53
|
+
with path.open('rb') as stream:
|
|
54
|
+
stream.seek(0, 2)
|
|
55
|
+
size = stream.tell()
|
|
56
|
+
stream.seek(max(0, size - limit))
|
|
57
|
+
data = stream.read(limit)
|
|
58
|
+
if size > limit and b'\n' in data:
|
|
59
|
+
data = data.split(b'\n', 1)[1]
|
|
60
|
+
return {'name': name, 'size': size, 'truncated': size > limit,
|
|
61
|
+
'text': data.decode('utf-8', errors='replace')}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class RollingLogWriter:
|
|
65
|
+
"""File-like sink: rotate daily or at 20 MiB, preserving open-stream users."""
|
|
66
|
+
encoding = 'utf-8'
|
|
67
|
+
|
|
68
|
+
def __init__(self, path, max_bytes=20 * 1024 * 1024):
|
|
69
|
+
self.path = Path(path)
|
|
70
|
+
self.max_bytes = max_bytes
|
|
71
|
+
self._lock = threading.RLock()
|
|
72
|
+
self._day = time.strftime('%Y-%m-%d', time.localtime(self.path.stat().st_mtime)) if self.path.exists() else time.strftime('%Y-%m-%d')
|
|
73
|
+
self._stream = self.path.open('a', encoding='utf-8', buffering=1)
|
|
74
|
+
|
|
75
|
+
def write(self, text):
|
|
76
|
+
with self._lock:
|
|
77
|
+
day = time.strftime('%Y-%m-%d')
|
|
78
|
+
if self._day != day or self._stream.tell() >= self.max_bytes:
|
|
79
|
+
self._stream.close()
|
|
80
|
+
try:
|
|
81
|
+
target = self.path.with_name('%s.%s.%s.log' % (self.path.stem, self._day, uuid.uuid4().hex[:12]))
|
|
82
|
+
self.path.rename(target)
|
|
83
|
+
self._day = day
|
|
84
|
+
finally:
|
|
85
|
+
self._stream = self.path.open('a', encoding='utf-8', buffering=1)
|
|
86
|
+
return self._stream.write(text)
|
|
87
|
+
|
|
88
|
+
def flush(self):
|
|
89
|
+
with self._lock:
|
|
90
|
+
self._stream.flush()
|
|
91
|
+
|
|
92
|
+
def close(self):
|
|
93
|
+
with self._lock:
|
|
94
|
+
self._stream.close()
|
|
95
|
+
|
|
96
|
+
def isatty(self):
|
|
97
|
+
return False
|
|
@@ -88,6 +88,8 @@ class NormalQmtBridge(TxTradeBridge):
|
|
|
88
88
|
self.callback_asset_dedupe_lock = threading.RLock()
|
|
89
89
|
self.callback_asset_fingerprints = {}
|
|
90
90
|
self.callback_asset_dedupe_max = 4096
|
|
91
|
+
self.order_terminal_statuses = {}
|
|
92
|
+
self.order_terminal_statuses_lock = threading.RLock()
|
|
91
93
|
|
|
92
94
|
def start(self):
|
|
93
95
|
if self.running:
|
|
@@ -773,6 +775,8 @@ class NormalQmtBridge(TxTradeBridge):
|
|
|
773
775
|
return
|
|
774
776
|
if event_name == "trader:on_stock_order":
|
|
775
777
|
data = self._format_trade_detail(obj, "order")
|
|
778
|
+
if not self._accept_order_callback(data):
|
|
779
|
+
return
|
|
776
780
|
elif event_name == "trader:on_stock_trade":
|
|
777
781
|
data = self._format_trade_detail(obj, "deal")
|
|
778
782
|
else:
|
|
@@ -836,6 +840,36 @@ class NormalQmtBridge(TxTradeBridge):
|
|
|
836
840
|
% (event_name, account_id or "-", not channel_duplicate, sent_clients, channel_duplicate and sent_clients == 0)
|
|
837
841
|
)
|
|
838
842
|
|
|
843
|
+
def _accept_order_callback(self, data):
|
|
844
|
+
"""Filter a stale partial-fill update emitted after a filled update."""
|
|
845
|
+
try:
|
|
846
|
+
status = int(data.get("order_status"))
|
|
847
|
+
except (TypeError, ValueError):
|
|
848
|
+
return True
|
|
849
|
+
partial = 55
|
|
850
|
+
succeeded = 56
|
|
851
|
+
if status not in (partial, succeeded):
|
|
852
|
+
return True
|
|
853
|
+
order_id = ""
|
|
854
|
+
for name in ("order_sysid", "order_id", "m_nRef", "m_nOrderID", "m_strOrderRef", "m_strOrderID"):
|
|
855
|
+
value = data.get(name)
|
|
856
|
+
if value not in (None, ""):
|
|
857
|
+
order_id = str(value).strip()
|
|
858
|
+
if order_id:
|
|
859
|
+
break
|
|
860
|
+
if not order_id:
|
|
861
|
+
return True
|
|
862
|
+
key = (str(data.get("account_id") or ""), order_id)
|
|
863
|
+
with self.order_terminal_statuses_lock:
|
|
864
|
+
if status == partial and self.order_terminal_statuses.get(key) == succeeded:
|
|
865
|
+
self._log("drop stale partial order callback account=%s order=%s" % key)
|
|
866
|
+
return False
|
|
867
|
+
if status == succeeded:
|
|
868
|
+
self.order_terminal_statuses[key] = succeeded
|
|
869
|
+
if len(self.order_terminal_statuses) > 4096:
|
|
870
|
+
self.order_terminal_statuses.pop(next(iter(self.order_terminal_statuses)))
|
|
871
|
+
return True
|
|
872
|
+
|
|
839
873
|
def _duplicate_asset_callback(self, scope, event_name, account_id, account_type, data):
|
|
840
874
|
if event_name != "trader:on_stock_asset" or not account_id:
|
|
841
875
|
return False
|
|
@@ -1140,6 +1140,8 @@ class CfquantQmtBridge(object):
|
|
|
1140
1140
|
"status_msg": self._first_value(obj, ("m_strStatusMsg", "m_strErrorMsg", "m_strCancelInfo", "m_strStatus", "m_strOrderStatus")),
|
|
1141
1141
|
"strategy_name": self._get_value(obj, "m_strStrategyName"),
|
|
1142
1142
|
"order_remark": self._first_value(obj, ("m_strRemark", "m_strOrderRemark")),
|
|
1143
|
+
"contract_no": self._first_value(obj, ("contract_no", "m_strCompactNo", "m_strContractNo", "m_strCompactID")),
|
|
1144
|
+
"stock_code1": self._first_value(obj, ("stock_code1", "m_stockCode", "m_strStockCode1", "m_strUnderCode")),
|
|
1143
1145
|
"m_strAccountID": self._get_value(obj, "m_strAccountID"),
|
|
1144
1146
|
"m_strInstrumentID": self._get_value(obj, "m_strInstrumentID"),
|
|
1145
1147
|
"m_strExchangeID": self._get_value(obj, "m_strExchangeID"),
|
|
@@ -1213,6 +1215,8 @@ class CfquantQmtBridge(object):
|
|
|
1213
1215
|
)),
|
|
1214
1216
|
"strategy_name": self._get_value(obj, "m_strStrategyName"),
|
|
1215
1217
|
"order_remark": self._first_value(obj, ("m_strRemark", "m_strOrderRemark")),
|
|
1218
|
+
"contract_no": self._first_value(obj, ("contract_no", "m_strCompactNo", "m_strContractNo", "m_strCompactID")),
|
|
1219
|
+
"stock_code1": self._first_value(obj, ("stock_code1", "m_stockCode", "m_strStockCode1", "m_strUnderCode")),
|
|
1216
1220
|
"direction": self._get_value(obj, "m_nDirection"),
|
|
1217
1221
|
"offset_flag": self._get_value(obj, "m_nOffsetFlag"),
|
|
1218
1222
|
"price": self._get_value(obj, "m_dPrice"),
|
|
@@ -10,6 +10,7 @@ import re
|
|
|
10
10
|
import threading
|
|
11
11
|
import time
|
|
12
12
|
import tokenize
|
|
13
|
+
import traceback
|
|
13
14
|
import uuid
|
|
14
15
|
from xml.dom import minidom
|
|
15
16
|
|
|
@@ -202,8 +203,13 @@ def _remove_formula_catalog_entries(document, names):
|
|
|
202
203
|
for catalog_group in document.getElementsByTagName("FormulaCatalog"):
|
|
203
204
|
for entry in list(catalog_group.getElementsByTagName("catalog")):
|
|
204
205
|
if entry.getAttribute("name") in names:
|
|
205
|
-
|
|
206
|
-
|
|
206
|
+
# QMT versions differ in whether catalog entries are direct
|
|
207
|
+
# children of FormulaCatalog or nested inside a sub-group.
|
|
208
|
+
# Always remove from the node that actually owns the entry.
|
|
209
|
+
parent = entry.parentNode
|
|
210
|
+
if parent is not None:
|
|
211
|
+
parent.removeChild(entry)
|
|
212
|
+
changed = True
|
|
207
213
|
return changed
|
|
208
214
|
|
|
209
215
|
|
|
@@ -329,8 +335,15 @@ class QmtStrategyManager:
|
|
|
329
335
|
_write_json(self.path, {"version": 1, "jobs": self.jobs, "errors": self.errors})
|
|
330
336
|
|
|
331
337
|
def report_error(self, account_key, error):
|
|
338
|
+
detail = str(error or "策略部署失败").strip() or "策略部署失败"
|
|
332
339
|
with self.lock:
|
|
333
|
-
self.errors[account_key] =
|
|
340
|
+
self.errors[account_key] = detail
|
|
341
|
+
# Keep the concrete exception on each affected job as well. The
|
|
342
|
+
# status endpoint previously returned only the generic state
|
|
343
|
+
# label, which made the Web UI hide the deployment root cause.
|
|
344
|
+
for job in self.jobs.values():
|
|
345
|
+
if job.get("account_key") == account_key:
|
|
346
|
+
job.update(state="error", error=detail, message="策略部署失败: " + detail, updated_at=time.time())
|
|
334
347
|
self._save()
|
|
335
348
|
return self.status(account_key)
|
|
336
349
|
|
|
@@ -338,6 +351,9 @@ class QmtStrategyManager:
|
|
|
338
351
|
if job.get("state") == state and job.get("error", "") == error:
|
|
339
352
|
return
|
|
340
353
|
job.update(state=state, message=error or STATES[state], error=error, updated_at=time.time())
|
|
354
|
+
if state != "error":
|
|
355
|
+
job.pop("deploy_retry_count", None)
|
|
356
|
+
job.pop("deploy_retry_after", None)
|
|
341
357
|
|
|
342
358
|
def _clear_generation_transition(self, job):
|
|
343
359
|
changed = False
|
|
@@ -423,7 +439,8 @@ class QmtStrategyManager:
|
|
|
423
439
|
"targets": [dict({k: job.get(k, "") for k in fields},
|
|
424
440
|
strategies=[r["name"] for r in job.get("roles", [])]) for job in jobs]}
|
|
425
441
|
if account_key in self.errors:
|
|
426
|
-
|
|
442
|
+
detail = str(self.errors[account_key] or "策略部署失败")
|
|
443
|
+
result.update(error=detail, message="账号已保存,策略部署失败: " + detail)
|
|
427
444
|
if self.load_error:
|
|
428
445
|
result.update(error=self.load_error, message=self.load_error)
|
|
429
446
|
return result
|
|
@@ -605,10 +622,26 @@ class QmtStrategyManager:
|
|
|
605
622
|
if not job.get("control_generation") and not _generation_values(
|
|
606
623
|
job.get("accepted_generations")):
|
|
607
624
|
continue
|
|
625
|
+
retry_count = int(job.get("deploy_retry_count") or 0)
|
|
626
|
+
if retry_count >= 5 or time.time() < float(job.get("deploy_retry_after") or 0):
|
|
627
|
+
continue
|
|
608
628
|
try:
|
|
609
629
|
self._advance(job)
|
|
610
630
|
except Exception as error:
|
|
611
|
-
|
|
631
|
+
retry_count = int(job.get("deploy_retry_count") or 0) + 1
|
|
632
|
+
job["deploy_retry_count"] = retry_count
|
|
633
|
+
job["deploy_retry_after"] = time.time() + min(10.0, 2.0 * retry_count)
|
|
634
|
+
detail = str(error).strip()
|
|
635
|
+
if not detail:
|
|
636
|
+
trace = traceback.format_exc().strip().splitlines()
|
|
637
|
+
frames = [line.strip() for line in trace if line.strip().startswith("File ")]
|
|
638
|
+
location = frames[-1] if frames else ""
|
|
639
|
+
detail = "%s (%s)" % (type(error).__name__, location or repr(error))
|
|
640
|
+
if retry_count < 5:
|
|
641
|
+
detail = "部署写入暂时失败(第 %d/5 次),系统将自动重试:%s" % (retry_count, detail)
|
|
642
|
+
else:
|
|
643
|
+
detail = "部署连续失败 5 次,请关闭 QMT 并检查目录权限后重新保存:%s" % detail
|
|
644
|
+
self._state(job, "error", detail)
|
|
612
645
|
changed = changed or before != job
|
|
613
646
|
if changed:
|
|
614
647
|
self._save()
|
|
@@ -666,11 +699,23 @@ class QmtStrategyManager:
|
|
|
666
699
|
imported = name in catalog and (root / "python" / (name + ".py")).is_file()
|
|
667
700
|
if not imported:
|
|
668
701
|
if not role.get("queued"):
|
|
669
|
-
|
|
670
|
-
|
|
702
|
+
generated = root / "python" / (name + ".py")
|
|
703
|
+
if generated.exists() or name in catalog:
|
|
704
|
+
# CFQ_* is cfquant's reserved namespace. A stale
|
|
705
|
+
# container/catalog entry can be left behind by a
|
|
706
|
+
# deployment on another machine; remove it while
|
|
707
|
+
# QMT is stopped and enqueue the current package.
|
|
708
|
+
# User strategies with other names still fail
|
|
709
|
+
# loudly instead of being overwritten.
|
|
710
|
+
if not _valid_managed_strategy_name(name):
|
|
711
|
+
raise ValueError("QMT 策略名称冲突: %s" % name)
|
|
712
|
+
changed = _remove_formula_catalog_entries(document, {name}) or changed
|
|
713
|
+
if generated.exists():
|
|
714
|
+
generated.unlink()
|
|
715
|
+
changed = True
|
|
671
716
|
package = Path(role["package_path"]).read_bytes()
|
|
672
717
|
if queue.exists() and queue.read_bytes() != package:
|
|
673
|
-
if role.get("reimport_required"):
|
|
718
|
+
if _valid_managed_strategy_name(name) or role.get("reimport_required"):
|
|
674
719
|
_atomic_write(queue, package)
|
|
675
720
|
else:
|
|
676
721
|
raise ValueError("QMT 导入队列存在同名文件: %s" % name)
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""default 账号多笔并发异步下单及回调数量核对。
|
|
3
|
+
|
|
4
|
+
运行:python -X utf8 "cfquant/tests/32_异步下单测试.py"
|
|
5
|
+
"""
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
import uuid
|
|
11
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from _helpers import PROJECT_ROOT, configure_stdout
|
|
15
|
+
from cfquant import configure, xtconstant
|
|
16
|
+
from cfquant.xttrader import XtQuantTrader, XtQuantTraderCallback
|
|
17
|
+
from cfquant.xttype import StockAccount
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
CONFIG_FILE = PROJECT_ROOT / "runtime/config/cfquant_web_config.json"
|
|
21
|
+
BRIDGE_ID = "default"
|
|
22
|
+
TRANSPORT = "auto"
|
|
23
|
+
ORDER_COUNT = 10
|
|
24
|
+
MAX_WORKERS = 5
|
|
25
|
+
STOCK_CODE = "000001.SZ"
|
|
26
|
+
ORDER_TYPE = xtconstant.STOCK_BUY
|
|
27
|
+
ORDER_VOLUME = 100
|
|
28
|
+
PRICE_TYPE = xtconstant.FIX_PRICE
|
|
29
|
+
PRICE = 10.0
|
|
30
|
+
REQUEST_TIMEOUT = 15.0
|
|
31
|
+
CALLBACK_TIMEOUT = 30.0
|
|
32
|
+
EXTRA_OBSERVE_SECONDS = 3.0
|
|
33
|
+
STRATEGY_NAME = "cfquant_async32"
|
|
34
|
+
DRY_RUN = False
|
|
35
|
+
|
|
36
|
+
PRINT_LOCK = threading.Lock()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def emit(event, **fields):
|
|
40
|
+
with PRINT_LOCK:
|
|
41
|
+
print(json.dumps({"event": event, **fields}, ensure_ascii=False, default=str), flush=True)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def plain(value):
|
|
45
|
+
return dict(value) if isinstance(value, dict) else dict(vars(value))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def value_of(value, *names):
|
|
49
|
+
for name in names:
|
|
50
|
+
item = value.get(name) if isinstance(value, dict) else getattr(value, name, None)
|
|
51
|
+
if item not in (None, ""):
|
|
52
|
+
return item
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def load_default_account():
|
|
57
|
+
data = json.loads(CONFIG_FILE.read_text(encoding="utf-8-sig"))
|
|
58
|
+
key = str(data.get("default_account_key") or "default:STOCK:").strip()
|
|
59
|
+
configs = data.get("account_configs") or {}
|
|
60
|
+
row = configs.get(key)
|
|
61
|
+
if not row:
|
|
62
|
+
matches = [item for item in configs.values() if item.get("bridge_id") == BRIDGE_ID and item.get("enabled") is not False]
|
|
63
|
+
if len(matches) != 1:
|
|
64
|
+
raise RuntimeError("无法唯一确定 default 启用账号")
|
|
65
|
+
row = matches[0]
|
|
66
|
+
if row.get("bridge_id") != BRIDGE_ID or row.get("account_type", "").upper() != "STOCK":
|
|
67
|
+
raise RuntimeError("default 配置不是启用的 STOCK 账号")
|
|
68
|
+
return row
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Callback(XtQuantTraderCallback):
|
|
72
|
+
def __init__(self, account_id):
|
|
73
|
+
self.account_id = str(account_id)
|
|
74
|
+
self.lock = threading.Lock()
|
|
75
|
+
self.responses = []
|
|
76
|
+
self.errors = []
|
|
77
|
+
|
|
78
|
+
def on_order_stock_async_response(self, response):
|
|
79
|
+
row = plain(response)
|
|
80
|
+
with self.lock:
|
|
81
|
+
self.responses.append(row)
|
|
82
|
+
emit("异步委托回调", seq=row.get("seq"), order_id=row.get("order_id"), order_remark=row.get("order_remark"))
|
|
83
|
+
|
|
84
|
+
def on_order_error(self, error):
|
|
85
|
+
row = plain(error)
|
|
86
|
+
with self.lock:
|
|
87
|
+
self.errors.append(row)
|
|
88
|
+
emit("下单错误回调", **row)
|
|
89
|
+
|
|
90
|
+
def snapshot(self):
|
|
91
|
+
with self.lock:
|
|
92
|
+
return list(self.responses), list(self.errors)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def main():
|
|
96
|
+
configure_stdout()
|
|
97
|
+
account_row = load_default_account()
|
|
98
|
+
account_id = str(account_row["account_id"])
|
|
99
|
+
remarks = ["async32_%s_%02d" % (uuid.uuid4().hex[:8], index + 1) for index in range(ORDER_COUNT)]
|
|
100
|
+
emit("测试配置", account_id=account_id, account_type=account_row.get("account_type"), bridge_id=BRIDGE_ID,
|
|
101
|
+
order_count=ORDER_COUNT, stock_code=STOCK_CODE, order_volume=ORDER_VOLUME, price=PRICE, dry_run=DRY_RUN)
|
|
102
|
+
if DRY_RUN:
|
|
103
|
+
return 0
|
|
104
|
+
|
|
105
|
+
configure(transport=TRANSPORT, bridge_id=BRIDGE_ID, timeout=REQUEST_TIMEOUT)
|
|
106
|
+
account = StockAccount(account_id, account_row["account_type"], BRIDGE_ID)
|
|
107
|
+
callback = Callback(account_id)
|
|
108
|
+
trader = XtQuantTrader(callback=callback, account=account)
|
|
109
|
+
trader.set_timeout(REQUEST_TIMEOUT)
|
|
110
|
+
submissions = []
|
|
111
|
+
|
|
112
|
+
def submit(index):
|
|
113
|
+
remark = remarks[index]
|
|
114
|
+
row = {"index": index + 1, "order_remark": remark}
|
|
115
|
+
try:
|
|
116
|
+
seq = trader.order_stock_async(account, STOCK_CODE, ORDER_TYPE, ORDER_VOLUME,
|
|
117
|
+
PRICE_TYPE, PRICE, STRATEGY_NAME, remark)
|
|
118
|
+
row.update(seq=seq, status="submitted" if seq not in (None, -1, "-1") else "rejected")
|
|
119
|
+
except Exception as error:
|
|
120
|
+
row.update(status="unknown", error="%s: %s" % (type(error).__name__, error))
|
|
121
|
+
emit("异步下单返回", **row)
|
|
122
|
+
return row
|
|
123
|
+
|
|
124
|
+
report_path = PROJECT_ROOT / "log" / ("async32_%s.json" % uuid.uuid4().hex[:10])
|
|
125
|
+
try:
|
|
126
|
+
trader.start()
|
|
127
|
+
if trader.connect() != 0:
|
|
128
|
+
raise RuntimeError("交易连接失败")
|
|
129
|
+
if trader.subscribe(account) != 0:
|
|
130
|
+
raise RuntimeError("账号订阅失败")
|
|
131
|
+
with ThreadPoolExecutor(max_workers=min(MAX_WORKERS, ORDER_COUNT)) as pool:
|
|
132
|
+
futures = [pool.submit(submit, index) for index in range(ORDER_COUNT)]
|
|
133
|
+
submissions = [future.result() for future in as_completed(futures)]
|
|
134
|
+
|
|
135
|
+
expected = {str(row["seq"]) for row in submissions if row["status"] == "submitted"}
|
|
136
|
+
deadline = time.time() + CALLBACK_TIMEOUT
|
|
137
|
+
while time.time() < deadline:
|
|
138
|
+
responses, _ = callback.snapshot()
|
|
139
|
+
received = {str(value_of(row, "seq", "m_nSeq")) for row in responses}
|
|
140
|
+
if expected.issubset(received):
|
|
141
|
+
break
|
|
142
|
+
time.sleep(0.1)
|
|
143
|
+
time.sleep(EXTRA_OBSERVE_SECONDS)
|
|
144
|
+
finally:
|
|
145
|
+
responses, errors = callback.snapshot()
|
|
146
|
+
expected = {str(row["seq"]) for row in submissions if row.get("status") == "submitted"}
|
|
147
|
+
received = [row for row in responses if str(value_of(row, "seq", "m_nSeq")) in expected]
|
|
148
|
+
counts = {}
|
|
149
|
+
for row in received:
|
|
150
|
+
seq = str(value_of(row, "seq", "m_nSeq"))
|
|
151
|
+
counts[seq] = counts.get(seq, 0) + 1
|
|
152
|
+
summary = {
|
|
153
|
+
"planned": ORDER_COUNT,
|
|
154
|
+
"submitted": len(expected),
|
|
155
|
+
"rejected": sum(row.get("status") == "rejected" for row in submissions),
|
|
156
|
+
"unknown": sum(row.get("status") == "unknown" for row in submissions),
|
|
157
|
+
"callback_count": len(received),
|
|
158
|
+
"missing_seqs": sorted(expected - set(counts)),
|
|
159
|
+
"duplicate_seqs": {seq: count for seq, count in counts.items() if count > 1},
|
|
160
|
+
"invalid_order_id_count": sum(value_of(row, "order_id", "m_nRef", "m_nOrderID") in (None, "", -1, "-1") for row in received),
|
|
161
|
+
}
|
|
162
|
+
summary["count_consistent"] = (
|
|
163
|
+
summary["submitted"] == summary["callback_count"]
|
|
164
|
+
and not summary["missing_seqs"]
|
|
165
|
+
and not summary["duplicate_seqs"]
|
|
166
|
+
)
|
|
167
|
+
report_path.parent.mkdir(parents=True, exist_ok=True)
|
|
168
|
+
report_path.write_text(json.dumps({"summary": summary, "submissions": submissions,
|
|
169
|
+
"responses": responses, "errors": errors}, ensure_ascii=False, indent=2, default=str), encoding="utf-8")
|
|
170
|
+
emit("测试汇总", **summary, report=str(report_path))
|
|
171
|
+
trader.stop()
|
|
172
|
+
return 0 if summary["count_consistent"] else 1
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
if __name__ == "__main__":
|
|
176
|
+
sys.exit(main())
|