cfquant 0.2.41__tar.gz → 0.2.42__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.41 → cfquant-0.2.42}/PKG-INFO +1 -1
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/normal_bridge.py +131 -1
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/pipe_bridge.py +1 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/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 +28 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tx_trade_bridge.py +160 -112
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/version.py +2 -2
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/xttype.py +16 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant.egg-info/PKG-INFO +1 -1
- {cfquant-0.2.41 → cfquant-0.2.42}/pyproject.toml +1 -1
- {cfquant-0.2.41 → cfquant-0.2.42}/qmt_scripts/CFQUANT.py +5 -4
- {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/app.js +1 -1
- {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/index.html +2 -2
- {cfquant-0.2.41 → cfquant-0.2.42}/LICENSE +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/LTtx/__init__.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/LTtx/tx/Config.txt +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/LTtx/tx/LTtx_server.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/LTtx/tx/__init__.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/LTtx/tx/tx.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/MANIFEST.in +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/README.md +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/__init__.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/_editable_install.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/account_routing.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/batch_orders.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/build_info.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/callback_dispatcher.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/cftrader.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/channels.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/cli.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/client.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/config.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/hub.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/level2.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/log_management.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/logging_i18n.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/order_meta.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/pipe_client.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/pipe_hub.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/pipe_transport.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/protocol.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/qmt_bridge.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/qmt_strategy_deploy.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/qmt_strategy_package.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/qmt_strategy_runtime.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/runtime_report.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/cfquant/tests/29_Level2/350/241/214/346/203/205/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/cfquant/tests/32_/345/274/202/346/255/245/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/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" +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/cfquant/tests/6_/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/cfquant/tests/_helpers.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/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" +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/readme.md +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_api_tester_browser.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_api_tester_web.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_callback_requests.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_cftrader.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_cftrader_tutorial.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_cftrader_web.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_credit_compat.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_json_serialization.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_log_management.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_management_batch.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_market_data_fallback.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_order_time_callback.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_project_updater.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_python_api_reference.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_qmt_strategy_browser.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_qmt_strategy_deploy.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_qmt_strategy_web.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_tutorial_reader.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_tx_auto_import.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tx.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/xtconstant.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/xtdata.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/xttrader.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant.egg-info/SOURCES.txt +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant.egg-info/dependency_links.txt +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant.egg-info/entry_points.txt +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant.egg-info/requires.txt +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant.egg-info/top_level.txt +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant_pipe_hub.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/cfquant_web_server.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/qmt_scripts/CFQUANT_LITE.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/qmt_scripts/__init__.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/requirements.txt +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/setup.cfg +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/__init__.py +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/api-tester.js +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/avatars/amber-pulse.svg +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/avatars/copper-grid.svg +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/avatars/market-blue.svg +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/avatars/rose-circuit.svg +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/avatars/signal-green.svg +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/avatars/slate-wave.svg +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/avatars/teal-orbit.svg +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/avatars/violet-node.svg +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/cftrader-api-data.js +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/favicon.svg +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/python-api-data.js +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/python-api.js +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/styles.css +0 -0
- {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/vendor/mermaid.min.js +0 -0
|
@@ -4,6 +4,7 @@ import hashlib
|
|
|
4
4
|
import json
|
|
5
5
|
import os
|
|
6
6
|
import queue
|
|
7
|
+
import re
|
|
7
8
|
import threading
|
|
8
9
|
import time
|
|
9
10
|
|
|
@@ -91,6 +92,8 @@ class NormalQmtBridge(TxTradeBridge):
|
|
|
91
92
|
self.callback_asset_dedupe_max = 4096
|
|
92
93
|
self.order_terminal_statuses = {}
|
|
93
94
|
self.order_terminal_statuses_lock = threading.RLock()
|
|
95
|
+
self.pending_order_errors = []
|
|
96
|
+
self.pending_order_errors_lock = threading.RLock()
|
|
94
97
|
|
|
95
98
|
def start(self):
|
|
96
99
|
if self.running:
|
|
@@ -141,6 +144,7 @@ class NormalQmtBridge(TxTradeBridge):
|
|
|
141
144
|
self._publish_runtime_report("context_ready")
|
|
142
145
|
|
|
143
146
|
def close(self):
|
|
147
|
+
self._flush_pending_order_errors(force=True)
|
|
144
148
|
self.running = False
|
|
145
149
|
self.worker_event.set()
|
|
146
150
|
self._close_quote_subscriptions()
|
|
@@ -627,6 +631,7 @@ class NormalQmtBridge(TxTradeBridge):
|
|
|
627
631
|
|
|
628
632
|
def on_timer(self, *args, **kwargs):
|
|
629
633
|
self._maybe_reset_order_meta_stores()
|
|
634
|
+
self._flush_pending_order_errors()
|
|
630
635
|
if self.dispatch_on_qmt_thread:
|
|
631
636
|
self._drain_requests("timer")
|
|
632
637
|
return
|
|
@@ -844,10 +849,13 @@ class NormalQmtBridge(TxTradeBridge):
|
|
|
844
849
|
def publish_callback_event(self, event_name, obj):
|
|
845
850
|
if self.tx is None:
|
|
846
851
|
return
|
|
852
|
+
force_order_error = isinstance(obj, dict) and obj.pop("_cfquant_force_order_error", False)
|
|
853
|
+
reconciled_order_error = None
|
|
847
854
|
if event_name == "trader:on_stock_order":
|
|
848
855
|
data = self._format_trade_detail(obj, "order")
|
|
849
856
|
if not self._accept_order_callback(data):
|
|
850
857
|
return
|
|
858
|
+
reconciled_order_error = self._match_pending_order_error(data)
|
|
851
859
|
elif event_name == "trader:on_stock_trade":
|
|
852
860
|
data = self._format_trade_detail(obj, "deal")
|
|
853
861
|
else:
|
|
@@ -860,6 +868,11 @@ class NormalQmtBridge(TxTradeBridge):
|
|
|
860
868
|
data.setdefault("account_id", account_id)
|
|
861
869
|
if account_type:
|
|
862
870
|
data.setdefault("account_type", account_type)
|
|
871
|
+
if event_name == "trader:on_order_error":
|
|
872
|
+
self._enrich_qmt_order_error_fields(data)
|
|
873
|
+
if not force_order_error:
|
|
874
|
+
self._queue_pending_order_error(data)
|
|
875
|
+
return
|
|
863
876
|
if event_name in (
|
|
864
877
|
"trader:on_stock_order",
|
|
865
878
|
"trader:on_stock_trade",
|
|
@@ -915,6 +928,115 @@ class NormalQmtBridge(TxTradeBridge):
|
|
|
915
928
|
"normal bridge callback event sent event=%s account=%s channel_sent=%s clients=%s duplicate=%s"
|
|
916
929
|
% (event_name, account_id or "-", not channel_duplicate, sent_clients, channel_duplicate and sent_clients == 0)
|
|
917
930
|
)
|
|
931
|
+
if reconciled_order_error is not None:
|
|
932
|
+
reconciled_order_error.update({
|
|
933
|
+
"order_id": data.get("order_id", -1),
|
|
934
|
+
"m_nOrderID": data.get("m_nOrderID", data.get("order_id", -1)),
|
|
935
|
+
"m_nRef": data.get("m_nRef", data.get("order_id", -1)),
|
|
936
|
+
"order_sysid": data.get("order_sysid", ""),
|
|
937
|
+
"stock_code": data.get("stock_code", reconciled_order_error.get("stock_code", "")),
|
|
938
|
+
"_cfquant_force_order_error": True,
|
|
939
|
+
})
|
|
940
|
+
self.publish_callback_event("trader:on_order_error", reconciled_order_error)
|
|
941
|
+
|
|
942
|
+
def _queue_pending_order_error(self, data):
|
|
943
|
+
with self.pending_order_errors_lock:
|
|
944
|
+
self.pending_order_errors.append({"data": dict(data), "created_at": time.time()})
|
|
945
|
+
|
|
946
|
+
def _match_pending_order_error(self, order):
|
|
947
|
+
if not isinstance(order, dict):
|
|
948
|
+
return None
|
|
949
|
+
account = str(order.get("account_id") or "").strip()
|
|
950
|
+
code = str(order.get("stock_code") or "").upper().split(".", 1)[0]
|
|
951
|
+
strategy = str(order.get("strategy_name") or "")
|
|
952
|
+
remark = str(order.get("order_remark") or "")
|
|
953
|
+
with self.pending_order_errors_lock:
|
|
954
|
+
for index, item in enumerate(self.pending_order_errors):
|
|
955
|
+
error = item.get("data") or {}
|
|
956
|
+
error_account = str(error.get("account_id") or "").strip()
|
|
957
|
+
error_code = str(error.get("stock_code") or "").upper().split(".", 1)[0]
|
|
958
|
+
if account and error_account and account != error_account:
|
|
959
|
+
continue
|
|
960
|
+
if code and error_code and code != error_code:
|
|
961
|
+
continue
|
|
962
|
+
error_strategy = str(error.get("strategy_name") or error.get("strategyName") or "")
|
|
963
|
+
if error_strategy and strategy and error_strategy != strategy and not strategy.startswith(error_strategy + "&&&"):
|
|
964
|
+
continue
|
|
965
|
+
error_remark = str(error.get("order_remark") or error.get("m_strRemark") or "")
|
|
966
|
+
if error_remark and remark and error_remark != remark:
|
|
967
|
+
continue
|
|
968
|
+
self.pending_order_errors.pop(index)
|
|
969
|
+
return error
|
|
970
|
+
return None
|
|
971
|
+
|
|
972
|
+
def _flush_pending_order_errors(self, force=False):
|
|
973
|
+
cutoff = time.time() - 0.5 if not force else time.time() + 1.0
|
|
974
|
+
expired = []
|
|
975
|
+
with self.pending_order_errors_lock:
|
|
976
|
+
keep = []
|
|
977
|
+
for item in self.pending_order_errors:
|
|
978
|
+
if item.get("created_at", 0) <= cutoff:
|
|
979
|
+
expired.append(item.get("data") or {})
|
|
980
|
+
else:
|
|
981
|
+
keep.append(item)
|
|
982
|
+
self.pending_order_errors = keep
|
|
983
|
+
for data in expired:
|
|
984
|
+
data["_cfquant_force_order_error"] = True
|
|
985
|
+
self.publish_callback_event("trader:on_order_error", data)
|
|
986
|
+
|
|
987
|
+
def _enrich_qmt_order_error_fields(self, data):
|
|
988
|
+
"""Fill canonical fields that大QMT only embeds in ``errMsg``.
|
|
989
|
+
|
|
990
|
+
The QMT strategy callback is ``orderError_callback(orderArgs, errMsg)``
|
|
991
|
+
and does not expose the MiniQMT ``XtOrderError`` structure. In
|
|
992
|
+
particular, counter errors commonly look like
|
|
993
|
+
``[COUNTER] [251005][...][p_stock_code=518880,...]``. Preserve any
|
|
994
|
+
native structured fields and only infer missing canonical values.
|
|
995
|
+
"""
|
|
996
|
+
if not isinstance(data, dict):
|
|
997
|
+
return data
|
|
998
|
+
# orderError_callback supplies QMT orderArgs names rather than the
|
|
999
|
+
# MiniQMT callback names. Preserve and canonicalize them first.
|
|
1000
|
+
if not data.get("account_id") and data.get("accountID"):
|
|
1001
|
+
data["account_id"] = data["accountID"]
|
|
1002
|
+
if not data.get("stock_code") and data.get("orderCode"):
|
|
1003
|
+
code = str(data["orderCode"]).strip().upper()
|
|
1004
|
+
if code.startswith(("SH", "SZ", "BJ")) and "." not in code:
|
|
1005
|
+
code = "%s.%s" % (code[2:], code[:2])
|
|
1006
|
+
data["stock_code"] = code
|
|
1007
|
+
if not data.get("strategy_name") and data.get("strategyName"):
|
|
1008
|
+
data["strategy_name"] = data["strategyName"]
|
|
1009
|
+
message = data.get("error_msg") or data.get("m_strErrorMsg") or data.get("message") or data.get("msg")
|
|
1010
|
+
if not message:
|
|
1011
|
+
return data
|
|
1012
|
+
message = str(message)
|
|
1013
|
+
if data.get("error_id") in (None, "", 0, "0") and data.get("m_nErrorID") in (None, "", 0, "0") and data.get("error_code") in (None, "", 0, "0"):
|
|
1014
|
+
for token in re.findall(r"\[(\d+)\]", message):
|
|
1015
|
+
try:
|
|
1016
|
+
error_id = int(token)
|
|
1017
|
+
except (TypeError, ValueError):
|
|
1018
|
+
continue
|
|
1019
|
+
if error_id:
|
|
1020
|
+
data["error_id"] = error_id
|
|
1021
|
+
break
|
|
1022
|
+
if not data.get("stock_code"):
|
|
1023
|
+
match = re.search(r"(?:^|[\[,;\s])p_stock_code\s*=\s*([A-Za-z0-9_.-]+)", message, re.IGNORECASE)
|
|
1024
|
+
if match:
|
|
1025
|
+
data["stock_code"] = match.group(1)
|
|
1026
|
+
internal_strategy = data.get("strategy_name") or data.get("strategyName")
|
|
1027
|
+
context = self._consume_order_error_context(
|
|
1028
|
+
data.get("account_id") or data.get("accountID"),
|
|
1029
|
+
data.get("stock_code") or data.get("orderCode"),
|
|
1030
|
+
internal_strategy,
|
|
1031
|
+
) if internal_strategy else None
|
|
1032
|
+
if context:
|
|
1033
|
+
data["strategy_name"] = context.get("strategy_name", "")
|
|
1034
|
+
data["m_strStrategyName"] = context.get("strategy_name", "")
|
|
1035
|
+
data["strategyName"] = context.get("strategy_name", "")
|
|
1036
|
+
data["order_remark"] = context.get("order_remark", "")
|
|
1037
|
+
data["m_strRemark"] = context.get("order_remark", "")
|
|
1038
|
+
data["cfquant_order_error_context_consumed"] = True
|
|
1039
|
+
return data
|
|
918
1040
|
|
|
919
1041
|
def _accept_order_callback(self, data):
|
|
920
1042
|
"""Filter a stale partial-fill update emitted after a filled update."""
|
|
@@ -974,7 +1096,8 @@ class NormalQmtBridge(TxTradeBridge):
|
|
|
974
1096
|
|
|
975
1097
|
def _callback_object_to_dict(self, obj):
|
|
976
1098
|
fields = [
|
|
977
|
-
"account_id",
|
|
1099
|
+
"account_id",
|
|
1100
|
+
"accountID",
|
|
978
1101
|
"account_type",
|
|
979
1102
|
"m_strAccountID",
|
|
980
1103
|
"m_strAccountId",
|
|
@@ -987,6 +1110,7 @@ class NormalQmtBridge(TxTradeBridge):
|
|
|
987
1110
|
"order_source",
|
|
988
1111
|
"source",
|
|
989
1112
|
"stock_code",
|
|
1113
|
+
"orderCode",
|
|
990
1114
|
"code",
|
|
991
1115
|
"market",
|
|
992
1116
|
"exchange_id",
|
|
@@ -1004,6 +1128,12 @@ class NormalQmtBridge(TxTradeBridge):
|
|
|
1004
1128
|
"order_remark",
|
|
1005
1129
|
"remark",
|
|
1006
1130
|
"strategy_name",
|
|
1131
|
+
"strategyName",
|
|
1132
|
+
"modelPrice",
|
|
1133
|
+
"modelVolume",
|
|
1134
|
+
"opType",
|
|
1135
|
+
"orderType",
|
|
1136
|
+
"prType",
|
|
1007
1137
|
"trade_id",
|
|
1008
1138
|
"deal_id",
|
|
1009
1139
|
"trade_time",
|
|
@@ -451,6 +451,34 @@ def test_order_error_callback_can_match_lightweight_request_before_passorder_ret
|
|
|
451
451
|
assert error.error_id == 103
|
|
452
452
|
|
|
453
453
|
|
|
454
|
+
def test_qmt_order_error_message_fills_missing_error_id_and_stock_code():
|
|
455
|
+
class RecordingTx(object):
|
|
456
|
+
def __init__(self):
|
|
457
|
+
self.pushes = []
|
|
458
|
+
|
|
459
|
+
def push(self, kind, payload, key):
|
|
460
|
+
self.pushes.append((kind, payload, key))
|
|
461
|
+
|
|
462
|
+
bridge = NormalQmtBridge(None, show=False, schedule_timer=False)
|
|
463
|
+
tx = RecordingTx()
|
|
464
|
+
bridge.tx = tx
|
|
465
|
+
try:
|
|
466
|
+
bridge.publish_callback_event("trader:on_order_error", {
|
|
467
|
+
"m_strAccountID": "A123",
|
|
468
|
+
"m_nAccountType": 2,
|
|
469
|
+
"order_source": "other",
|
|
470
|
+
"m_strErrorMsg": "[COUNTER] [251005][证券可用数量不足][p_stock_code=518880,p_enable_amount=0]",
|
|
471
|
+
})
|
|
472
|
+
finally:
|
|
473
|
+
bridge.close()
|
|
474
|
+
|
|
475
|
+
payload = json.loads([item for item in tx.pushes if item[0] == "event"][-1][1])
|
|
476
|
+
data = payload["data"]
|
|
477
|
+
assert data["error_id"] == 251005
|
|
478
|
+
assert data["stock_code"] == "518880"
|
|
479
|
+
assert XtOrderError.from_any(data).error_id == 251005
|
|
480
|
+
|
|
481
|
+
|
|
454
482
|
@pytest.mark.parametrize("bridge_class", [NormalQmtBridge, PipeNormalQmtBridge])
|
|
455
483
|
def test_cancel_error_callback_restores_context_by_order_id(bridge_class):
|
|
456
484
|
class RecordingTx(object):
|
|
@@ -3,8 +3,9 @@ import os
|
|
|
3
3
|
import inspect
|
|
4
4
|
import json
|
|
5
5
|
import sys
|
|
6
|
-
import threading
|
|
7
|
-
import time
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
import uuid
|
|
8
9
|
|
|
9
10
|
from .protocol import loads_message, pack_event, pack_response
|
|
10
11
|
from .batch_orders import (
|
|
@@ -42,53 +43,53 @@ _LOADED_SOURCE_SHA256 = source_sha256(__file__)
|
|
|
42
43
|
# bridge against the same ContextInfo. QMT treats every
|
|
43
44
|
# set_auto_trade_callback(True) call as a new registration, so keep the
|
|
44
45
|
# registration state shared across bridge instances.
|
|
45
|
-
_AUTO_TRADE_CALLBACK_LOCK = threading.RLock()
|
|
46
|
-
_AUTO_TRADE_CALLBACK_REGISTRY = {}
|
|
47
|
-
|
|
48
|
-
# A normal bridge and a trade bridge can be created for the same QMT context.
|
|
49
|
-
# Keep a small relay registry so a stock-order callback received by either
|
|
50
|
-
# entry point can wake the synchronous order resolver immediately.
|
|
51
|
-
_SYNC_ORDER_CALLBACK_RELAY_LOCK = threading.RLock()
|
|
52
|
-
_SYNC_ORDER_CALLBACK_RELAYS = {}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def _register_sync_order_callback_relay(bridge):
|
|
56
|
-
context = getattr(bridge, "context", None)
|
|
57
|
-
if context is None:
|
|
58
|
-
return
|
|
59
|
-
key = id(context)
|
|
60
|
-
with _SYNC_ORDER_CALLBACK_RELAY_LOCK:
|
|
61
|
-
bucket = _SYNC_ORDER_CALLBACK_RELAYS.setdefault(key, [])
|
|
62
|
-
if bridge not in bucket:
|
|
63
|
-
bucket.append(bridge)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def _unregister_sync_order_callback_relay(bridge):
|
|
67
|
-
context = getattr(bridge, "context", None)
|
|
68
|
-
if context is None:
|
|
69
|
-
return
|
|
70
|
-
key = id(context)
|
|
71
|
-
with _SYNC_ORDER_CALLBACK_RELAY_LOCK:
|
|
72
|
-
bucket = _SYNC_ORDER_CALLBACK_RELAYS.get(key, [])
|
|
73
|
-
bucket[:] = [item for item in bucket if item is not bridge]
|
|
74
|
-
if not bucket:
|
|
75
|
-
_SYNC_ORDER_CALLBACK_RELAYS.pop(key, None)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def relay_sync_order_callback(context, order):
|
|
79
|
-
"""Forward a formatted order callback to trade bridges sharing context."""
|
|
80
|
-
if context is None:
|
|
81
|
-
return 0
|
|
82
|
-
with _SYNC_ORDER_CALLBACK_RELAY_LOCK:
|
|
83
|
-
bridges = list(_SYNC_ORDER_CALLBACK_RELAYS.get(id(context), ()))
|
|
84
|
-
matched = 0
|
|
85
|
-
for bridge in bridges:
|
|
86
|
-
try:
|
|
87
|
-
if bridge._resolve_pending_sync_order_callback(order):
|
|
88
|
-
matched += 1
|
|
89
|
-
except Exception:
|
|
90
|
-
continue
|
|
91
|
-
return matched
|
|
46
|
+
_AUTO_TRADE_CALLBACK_LOCK = threading.RLock()
|
|
47
|
+
_AUTO_TRADE_CALLBACK_REGISTRY = {}
|
|
48
|
+
|
|
49
|
+
# A normal bridge and a trade bridge can be created for the same QMT context.
|
|
50
|
+
# Keep a small relay registry so a stock-order callback received by either
|
|
51
|
+
# entry point can wake the synchronous order resolver immediately.
|
|
52
|
+
_SYNC_ORDER_CALLBACK_RELAY_LOCK = threading.RLock()
|
|
53
|
+
_SYNC_ORDER_CALLBACK_RELAYS = {}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _register_sync_order_callback_relay(bridge):
|
|
57
|
+
context = getattr(bridge, "context", None)
|
|
58
|
+
if context is None:
|
|
59
|
+
return
|
|
60
|
+
key = id(context)
|
|
61
|
+
with _SYNC_ORDER_CALLBACK_RELAY_LOCK:
|
|
62
|
+
bucket = _SYNC_ORDER_CALLBACK_RELAYS.setdefault(key, [])
|
|
63
|
+
if bridge not in bucket:
|
|
64
|
+
bucket.append(bridge)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _unregister_sync_order_callback_relay(bridge):
|
|
68
|
+
context = getattr(bridge, "context", None)
|
|
69
|
+
if context is None:
|
|
70
|
+
return
|
|
71
|
+
key = id(context)
|
|
72
|
+
with _SYNC_ORDER_CALLBACK_RELAY_LOCK:
|
|
73
|
+
bucket = _SYNC_ORDER_CALLBACK_RELAYS.get(key, [])
|
|
74
|
+
bucket[:] = [item for item in bucket if item is not bridge]
|
|
75
|
+
if not bucket:
|
|
76
|
+
_SYNC_ORDER_CALLBACK_RELAYS.pop(key, None)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def relay_sync_order_callback(context, order):
|
|
80
|
+
"""Forward a formatted order callback to trade bridges sharing context."""
|
|
81
|
+
if context is None:
|
|
82
|
+
return 0
|
|
83
|
+
with _SYNC_ORDER_CALLBACK_RELAY_LOCK:
|
|
84
|
+
bridges = list(_SYNC_ORDER_CALLBACK_RELAYS.get(id(context), ()))
|
|
85
|
+
matched = 0
|
|
86
|
+
for bridge in bridges:
|
|
87
|
+
try:
|
|
88
|
+
if bridge._resolve_pending_sync_order_callback(order):
|
|
89
|
+
matched += 1
|
|
90
|
+
except Exception:
|
|
91
|
+
continue
|
|
92
|
+
return matched
|
|
92
93
|
|
|
93
94
|
|
|
94
95
|
XTTRADER_COMPAT_CANDIDATES = {
|
|
@@ -237,22 +238,24 @@ class TxTradeBridge(object):
|
|
|
237
238
|
self.pending_async_orders_lock = threading.RLock()
|
|
238
239
|
self.pending_sync_orders = []
|
|
239
240
|
self.pending_sync_orders_lock = threading.RLock()
|
|
240
|
-
self.order_request_metadata = {}
|
|
241
|
-
self.order_request_metadata_lock = threading.RLock()
|
|
241
|
+
self.order_request_metadata = {}
|
|
242
|
+
self.order_request_metadata_lock = threading.RLock()
|
|
243
|
+
self.order_error_contexts = {}
|
|
244
|
+
self.order_error_contexts_lock = threading.RLock()
|
|
242
245
|
self.order_meta_enabled = bool(order_meta_enabled)
|
|
243
246
|
self.order_meta_cache = order_meta.OrderMetaCache(self.bridge_id)
|
|
244
247
|
self.order_meta_store_lock = threading.RLock()
|
|
245
248
|
self.order_meta_store_initialized = set()
|
|
246
249
|
|
|
247
|
-
def set_context(self, context):
|
|
248
|
-
if self.context is not None and self.context is not context:
|
|
249
|
-
self._release_auto_trade_callback(self.context)
|
|
250
|
-
_unregister_sync_order_callback_relay(self)
|
|
251
|
-
self.context = context
|
|
250
|
+
def set_context(self, context):
|
|
251
|
+
if self.context is not None and self.context is not context:
|
|
252
|
+
self._release_auto_trade_callback(self.context)
|
|
253
|
+
_unregister_sync_order_callback_relay(self)
|
|
254
|
+
self.context = context
|
|
252
255
|
if self.account_id:
|
|
253
256
|
self._set_context_account(self.account_id, self.account_type)
|
|
254
|
-
self._enable_auto_trade_callback()
|
|
255
|
-
_register_sync_order_callback_relay(self)
|
|
257
|
+
self._enable_auto_trade_callback()
|
|
258
|
+
_register_sync_order_callback_relay(self)
|
|
256
259
|
self._log("tx trade bridge context ready")
|
|
257
260
|
self._publish_runtime_report("context_ready")
|
|
258
261
|
|
|
@@ -273,10 +276,10 @@ class TxTradeBridge(object):
|
|
|
273
276
|
self._publish_runtime_report("start")
|
|
274
277
|
return self
|
|
275
278
|
|
|
276
|
-
def close(self):
|
|
277
|
-
self.running = False
|
|
278
|
-
self._release_auto_trade_callback()
|
|
279
|
-
_unregister_sync_order_callback_relay(self)
|
|
279
|
+
def close(self):
|
|
280
|
+
self.running = False
|
|
281
|
+
self._release_auto_trade_callback()
|
|
282
|
+
_unregister_sync_order_callback_relay(self)
|
|
280
283
|
tx = self.tx
|
|
281
284
|
self.tx = None
|
|
282
285
|
if tx is not None:
|
|
@@ -981,7 +984,13 @@ class TxTradeBridge(object):
|
|
|
981
984
|
("order_remark", "remark", "strategy_name"),
|
|
982
985
|
msg.get("id", "tx_order"),
|
|
983
986
|
)
|
|
984
|
-
strategy_name = params.get("strategy_name", "")
|
|
987
|
+
strategy_name = params.get("strategy_name", "")
|
|
988
|
+
qmt_strategy_name = self._register_order_error_context(
|
|
989
|
+
account_id,
|
|
990
|
+
params.get("stock_code", params.get("code", "")),
|
|
991
|
+
strategy_name,
|
|
992
|
+
order_remark,
|
|
993
|
+
)
|
|
985
994
|
order_meta_record = None
|
|
986
995
|
if self.order_meta_enabled:
|
|
987
996
|
order_meta_record = self._build_order_meta_record(
|
|
@@ -1025,12 +1034,13 @@ class TxTradeBridge(object):
|
|
|
1025
1034
|
price_type,
|
|
1026
1035
|
params.get("price", 0),
|
|
1027
1036
|
params.get("order_volume", params.get("num", 0)),
|
|
1028
|
-
|
|
1037
|
+
qmt_strategy_name,
|
|
1029
1038
|
params.get("quick_trade", 2),
|
|
1030
1039
|
order_remark,
|
|
1031
1040
|
self.context,
|
|
1032
1041
|
)
|
|
1033
|
-
except Exception as e:
|
|
1042
|
+
except Exception as e:
|
|
1043
|
+
self._discard_order_error_context(account_id, params.get("stock_code", params.get("code", "")), qmt_strategy_name)
|
|
1034
1044
|
if pending_sync_order is not None:
|
|
1035
1045
|
self._discard_pending_sync_order(pending_sync_order)
|
|
1036
1046
|
if order_meta_record is not None:
|
|
@@ -1110,7 +1120,45 @@ class TxTradeBridge(object):
|
|
|
1110
1120
|
"previous_order_id": previous_order_id,
|
|
1111
1121
|
}
|
|
1112
1122
|
|
|
1113
|
-
|
|
1123
|
+
@staticmethod
|
|
1124
|
+
def _order_error_code(stock_code):
|
|
1125
|
+
text = str(stock_code or "").strip().upper()
|
|
1126
|
+
if text.startswith(("SH", "SZ", "BJ")) and "." not in text and len(text) > 2:
|
|
1127
|
+
text = text[2:]
|
|
1128
|
+
return text.split(".", 1)[0]
|
|
1129
|
+
|
|
1130
|
+
def _register_order_error_context(self, account_id, stock_code, strategy_name, order_remark):
|
|
1131
|
+
original = str(strategy_name or "")
|
|
1132
|
+
internal = "%s&&&_cfq_%s" % (original, uuid.uuid4().hex[:12]) if original else "cfq_%s" % uuid.uuid4().hex[:12]
|
|
1133
|
+
key = (str(account_id or "").strip(), self._order_error_code(stock_code), internal)
|
|
1134
|
+
now = time.time()
|
|
1135
|
+
with self.order_error_contexts_lock:
|
|
1136
|
+
cutoff = now - 300.0
|
|
1137
|
+
self.order_error_contexts = {
|
|
1138
|
+
item_key: item for item_key, item in self.order_error_contexts.items()
|
|
1139
|
+
if item.get("created_at", now) >= cutoff
|
|
1140
|
+
}
|
|
1141
|
+
self.order_error_contexts[key] = {
|
|
1142
|
+
"account_id": key[0], "stock_code": key[1],
|
|
1143
|
+
"internal_strategy_name": internal, "strategy_name": original,
|
|
1144
|
+
"order_remark": str(order_remark or ""), "created_at": now,
|
|
1145
|
+
"lifecycle": "pending",
|
|
1146
|
+
}
|
|
1147
|
+
while len(self.order_error_contexts) > 4096:
|
|
1148
|
+
self.order_error_contexts.pop(next(iter(self.order_error_contexts)))
|
|
1149
|
+
return internal
|
|
1150
|
+
|
|
1151
|
+
def _discard_order_error_context(self, account_id, stock_code, internal_strategy):
|
|
1152
|
+
key = (str(account_id or "").strip(), self._order_error_code(stock_code), str(internal_strategy or ""))
|
|
1153
|
+
with self.order_error_contexts_lock:
|
|
1154
|
+
self.order_error_contexts.pop(key, None)
|
|
1155
|
+
|
|
1156
|
+
def _consume_order_error_context(self, account_id, stock_code, internal_strategy):
|
|
1157
|
+
key = (str(account_id or "").strip(), self._order_error_code(stock_code), str(internal_strategy or ""))
|
|
1158
|
+
with self.order_error_contexts_lock:
|
|
1159
|
+
return self.order_error_contexts.pop(key, None)
|
|
1160
|
+
|
|
1161
|
+
def _register_pending_sync_order(
|
|
1114
1162
|
self,
|
|
1115
1163
|
account_id,
|
|
1116
1164
|
account_type,
|
|
@@ -1129,18 +1177,18 @@ class TxTradeBridge(object):
|
|
|
1129
1177
|
"created_at": time.time(),
|
|
1130
1178
|
"event": threading.Event(),
|
|
1131
1179
|
}
|
|
1132
|
-
with self.pending_sync_orders_lock:
|
|
1133
|
-
self.pending_sync_orders.append(record)
|
|
1134
|
-
_register_sync_order_callback_relay(self)
|
|
1135
|
-
return record
|
|
1180
|
+
with self.pending_sync_orders_lock:
|
|
1181
|
+
self.pending_sync_orders.append(record)
|
|
1182
|
+
_register_sync_order_callback_relay(self)
|
|
1183
|
+
return record
|
|
1136
1184
|
|
|
1137
1185
|
def _discard_pending_sync_order(self, record):
|
|
1138
1186
|
if record is None:
|
|
1139
1187
|
return
|
|
1140
|
-
with self.pending_sync_orders_lock:
|
|
1141
|
-
self.pending_sync_orders[:] = [
|
|
1142
|
-
item for item in self.pending_sync_orders if item is not record
|
|
1143
|
-
]
|
|
1188
|
+
with self.pending_sync_orders_lock:
|
|
1189
|
+
self.pending_sync_orders[:] = [
|
|
1190
|
+
item for item in self.pending_sync_orders if item is not record
|
|
1191
|
+
]
|
|
1144
1192
|
|
|
1145
1193
|
def _resolve_pending_sync_order_callback(self, order):
|
|
1146
1194
|
"""Wake a synchronous resolver when QMT publishes the matching order.
|
|
@@ -1175,21 +1223,21 @@ class TxTradeBridge(object):
|
|
|
1175
1223
|
expected_strategy = str(record.get("strategy_name") or "")
|
|
1176
1224
|
if not order_remark and strategy_name and expected_strategy and strategy_name != expected_strategy:
|
|
1177
1225
|
continue
|
|
1178
|
-
if self._is_previous_order_detail(order, record.get("previous_order_id")):
|
|
1179
|
-
continue
|
|
1180
|
-
# Some QMT builds include the internal reference directly in
|
|
1181
|
-
# the callback. Keep it so the synchronous path can return
|
|
1182
|
-
# without a full ORDER-history query.
|
|
1183
|
-
callback_order_id = None
|
|
1184
|
-
for name in ("m_nRef", "m_nOrderID", "order_id"):
|
|
1185
|
-
callback_order_id = self._normalize_order_id(self._get_value(order, name))
|
|
1186
|
-
if callback_order_id is not None:
|
|
1187
|
-
break
|
|
1188
|
-
previous_key = self._order_reference_key(record.get("previous_order_id"))
|
|
1189
|
-
if callback_order_id is not None and self._order_reference_key(callback_order_id) != previous_key:
|
|
1190
|
-
record["callback_order_id"] = callback_order_id
|
|
1191
|
-
if order_sysid not in (None, ""):
|
|
1192
|
-
record["callback_order_sysid"] = str(order_sysid)
|
|
1226
|
+
if self._is_previous_order_detail(order, record.get("previous_order_id")):
|
|
1227
|
+
continue
|
|
1228
|
+
# Some QMT builds include the internal reference directly in
|
|
1229
|
+
# the callback. Keep it so the synchronous path can return
|
|
1230
|
+
# without a full ORDER-history query.
|
|
1231
|
+
callback_order_id = None
|
|
1232
|
+
for name in ("m_nRef", "m_nOrderID", "order_id"):
|
|
1233
|
+
callback_order_id = self._normalize_order_id(self._get_value(order, name))
|
|
1234
|
+
if callback_order_id is not None:
|
|
1235
|
+
break
|
|
1236
|
+
previous_key = self._order_reference_key(record.get("previous_order_id"))
|
|
1237
|
+
if callback_order_id is not None and self._order_reference_key(callback_order_id) != previous_key:
|
|
1238
|
+
record["callback_order_id"] = callback_order_id
|
|
1239
|
+
if order_sysid not in (None, ""):
|
|
1240
|
+
record["callback_order_sysid"] = str(order_sysid)
|
|
1193
1241
|
record["callback_seen_at"] = time.time()
|
|
1194
1242
|
record["event"].set()
|
|
1195
1243
|
return True
|
|
@@ -1218,27 +1266,27 @@ class TxTradeBridge(object):
|
|
|
1218
1266
|
wait_seconds = max(0.0, float(wait_seconds or 0))
|
|
1219
1267
|
except Exception:
|
|
1220
1268
|
wait_seconds = 2.0
|
|
1221
|
-
deadline = time.time() + wait_seconds
|
|
1222
|
-
while True:
|
|
1223
|
-
if pending_sync_order is not None:
|
|
1224
|
-
callback_order_id = pending_sync_order.get("callback_order_id")
|
|
1225
|
-
if callback_order_id is not None:
|
|
1226
|
-
return callback_order_id
|
|
1227
|
-
# A callback is the cheap readiness signal. Waiting for it before
|
|
1228
|
-
# querying the complete ORDER list avoids repeatedly transferring
|
|
1229
|
-
# multi-megabyte histories through QMT while the new order is still
|
|
1230
|
-
# being committed. A zero wait keeps the one-shot lookup behavior
|
|
1231
|
-
# used by callers that explicitly disable waiting.
|
|
1232
|
-
if (
|
|
1233
|
-
pending_sync_order is not None
|
|
1234
|
-
and wait_seconds > 0
|
|
1235
|
-
and not pending_sync_order.get("callback_seen_at")
|
|
1236
|
-
and time.time() < deadline
|
|
1237
|
-
):
|
|
1238
|
-
pending_sync_order["event"].wait(min(0.05, max(0.0, deadline - time.time())))
|
|
1239
|
-
pending_sync_order["event"].clear()
|
|
1240
|
-
continue
|
|
1241
|
-
try:
|
|
1269
|
+
deadline = time.time() + wait_seconds
|
|
1270
|
+
while True:
|
|
1271
|
+
if pending_sync_order is not None:
|
|
1272
|
+
callback_order_id = pending_sync_order.get("callback_order_id")
|
|
1273
|
+
if callback_order_id is not None:
|
|
1274
|
+
return callback_order_id
|
|
1275
|
+
# A callback is the cheap readiness signal. Waiting for it before
|
|
1276
|
+
# querying the complete ORDER list avoids repeatedly transferring
|
|
1277
|
+
# multi-megabyte histories through QMT while the new order is still
|
|
1278
|
+
# being committed. A zero wait keeps the one-shot lookup behavior
|
|
1279
|
+
# used by callers that explicitly disable waiting.
|
|
1280
|
+
if (
|
|
1281
|
+
pending_sync_order is not None
|
|
1282
|
+
and wait_seconds > 0
|
|
1283
|
+
and not pending_sync_order.get("callback_seen_at")
|
|
1284
|
+
and time.time() < deadline
|
|
1285
|
+
):
|
|
1286
|
+
pending_sync_order["event"].wait(min(0.05, max(0.0, deadline - time.time())))
|
|
1287
|
+
pending_sync_order["event"].clear()
|
|
1288
|
+
continue
|
|
1289
|
+
try:
|
|
1242
1290
|
orders = self._query_trade_detail({
|
|
1243
1291
|
"account": {"account_id": account_id, "account_type": account_type},
|
|
1244
1292
|
}, "order")
|
|
@@ -1295,8 +1343,8 @@ class TxTradeBridge(object):
|
|
|
1295
1343
|
# returned by order queries/callbacks. Wait for the matching detail.
|
|
1296
1344
|
if time.time() >= deadline:
|
|
1297
1345
|
return None
|
|
1298
|
-
if pending_sync_order is not None:
|
|
1299
|
-
pending_sync_order["event"].wait(min(0.05, max(0.0, deadline - time.time())))
|
|
1346
|
+
if pending_sync_order is not None:
|
|
1347
|
+
pending_sync_order["event"].wait(min(0.05, max(0.0, deadline - time.time())))
|
|
1300
1348
|
pending_sync_order["event"].clear()
|
|
1301
1349
|
else:
|
|
1302
1350
|
time.sleep(0.05)
|
|
@@ -1209,6 +1209,22 @@ class XtOrderError(DictObject):
|
|
|
1209
1209
|
_set_first(data, "strategy_name", ("m_strStrategyName",), default="")
|
|
1210
1210
|
_set_first(data, "order_remark", ("m_strRemark", "m_strOrderRemark"), default="")
|
|
1211
1211
|
_normalize_order_id_field(data)
|
|
1212
|
+
# 大 QMT 的 orderError_callback 只提供错误文本,柜台错误码和股票代码
|
|
1213
|
+
# 常以内嵌字段出现,例如 [251005]... [p_stock_code=518880,...]。
|
|
1214
|
+
# 仅在标准字段缺失时补充,保留 MiniQMT 原生字段优先级。
|
|
1215
|
+
if data.get("error_id") in (None, "", 0, "0"):
|
|
1216
|
+
for token in re.findall(r"\[(\d+)\]", str(data.get("error_msg") or "")):
|
|
1217
|
+
if int(token):
|
|
1218
|
+
data["error_id"] = int(token)
|
|
1219
|
+
break
|
|
1220
|
+
if not data.get("stock_code"):
|
|
1221
|
+
match = re.search(
|
|
1222
|
+
r"(?:^|[\[,;\s])p_stock_code\s*=\s*([A-Za-z0-9_.-]+)",
|
|
1223
|
+
str(data.get("error_msg") or ""),
|
|
1224
|
+
re.IGNORECASE,
|
|
1225
|
+
)
|
|
1226
|
+
if match:
|
|
1227
|
+
data["stock_code"] = match.group(1)
|
|
1212
1228
|
if data.get("order_id") in (0, "0", -1, "-1"):
|
|
1213
1229
|
data["order_id"] = -1
|
|
1214
1230
|
data["account_id"] = _coerce_text(data.get("account_id"))
|