cfquant 0.2.51__tar.gz → 0.2.52__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.51 → cfquant-0.2.52}/PKG-INFO +1 -1
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/client.py +9 -5
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/level2.py +7 -1
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/normal_bridge.py +3 -2
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/pipe_bridge.py +6 -2
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/pipe_client.py +9 -5
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/protocol.py +65 -18
- cfquant-0.2.52/cfquant/qmt_binding_settings.py +39 -0
- cfquant-0.2.52/cfquant/rpc_timing.py +45 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/28_Level2/346/216/245/345/217/243/351/200/202/351/205/215/346/265/213/350/257/225.py +6 -3
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_api_tester_browser.py +103 -8
- cfquant-0.2.52/cfquant/tests/test_binding_runtime_browser.py +109 -0
- cfquant-0.2.52/cfquant/tests/test_dataframe_encoding.py +192 -0
- cfquant-0.2.52/cfquant/tests/test_lite_qmt_dispatch.py +266 -0
- cfquant-0.2.52/cfquant/tests/test_market_timing.py +238 -0
- cfquant-0.2.52/cfquant/tests/test_qmt_binding_settings.py +123 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_qmt_fileless.py +14 -3
- cfquant-0.2.52/cfquant/tests/test_table_conversion.py +122 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tx_trade_bridge.py +91 -44
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/version.py +2 -2
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant.egg-info/PKG-INFO +1 -1
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant.egg-info/SOURCES.txt +8 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant_web_server.py +109 -28
- {cfquant-0.2.51 → cfquant-0.2.52}/pyproject.toml +1 -1
- {cfquant-0.2.51 → cfquant-0.2.52}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +33 -5
- {cfquant-0.2.51 → cfquant-0.2.52}/qmt_scripts/CFQUANT_LITE.py +298 -62
- {cfquant-0.2.51 → cfquant-0.2.52}/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 +33 -5
- {cfquant-0.2.51 → cfquant-0.2.52}/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 +33 -5
- {cfquant-0.2.51 → cfquant-0.2.52}/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 +298 -62
- {cfquant-0.2.51 → cfquant-0.2.52}/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 +298 -62
- {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/app.js +104 -6
- {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/index.html +24 -2
- {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/styles.css +58 -15
- {cfquant-0.2.51 → cfquant-0.2.52}/LICENSE +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/LTtx/__init__.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/LTtx/tx/Config.txt +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/LTtx/tx/LTtx_server.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/LTtx/tx/__init__.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/LTtx/tx/tx.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/MANIFEST.in +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/README.md +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/__init__.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/_editable_install.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/account_routing.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/batch_orders.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/build_info.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/callback_dispatcher.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/cftrader.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/channels.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/cli.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/config.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/hub.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/log_management.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/logging_i18n.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/management.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/order_identity.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/order_meta.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/pipe_hub.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/pipe_transport.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/qmt_bridge.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/qmt_pipe_runtime.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/qmt_strategy_deploy.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/qmt_strategy_package.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/qmt_strategy_runtime.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/runtime_report.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/stock_connect.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/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.51 → cfquant-0.2.52}/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.51 → cfquant-0.2.52}/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.51 → cfquant-0.2.52}/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.51 → cfquant-0.2.52}/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.51 → cfquant-0.2.52}/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.51 → cfquant-0.2.52}/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.51 → cfquant-0.2.52}/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.51 → cfquant-0.2.52}/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.51 → cfquant-0.2.52}/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.51 → cfquant-0.2.52}/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.51 → cfquant-0.2.52}/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.51 → cfquant-0.2.52}/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.51 → cfquant-0.2.52}/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.51 → cfquant-0.2.52}/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.51 → cfquant-0.2.52}/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.51 → cfquant-0.2.52}/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.51 → cfquant-0.2.52}/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.51 → cfquant-0.2.52}/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.51 → cfquant-0.2.52}/cfquant/tests/29_Level2/350/241/214/346/203/205/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/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.51 → cfquant-0.2.52}/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.51 → cfquant-0.2.52}/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.51 → cfquant-0.2.52}/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.51 → cfquant-0.2.52}/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.51 → cfquant-0.2.52}/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.51 → cfquant-0.2.52}/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.51 → cfquant-0.2.52}/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.51 → cfquant-0.2.52}/cfquant/tests/6_/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/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.51 → cfquant-0.2.52}/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.51 → cfquant-0.2.52}/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.51 → cfquant-0.2.52}/cfquant/tests/_helpers.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/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.51 → cfquant-0.2.52}/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.51 → cfquant-0.2.52}/cfquant/tests/readme.md +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_advanced_parity.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_api_tester_web.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_callback_requests.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_cftrader.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_cftrader_tutorial.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_cftrader_web.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_credit_compat.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_json_serialization.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_lite_account_routing.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_lite_deployment_paths.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_lite_parity.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_local_trading_calendar.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_log_management.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_management.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_management_batch.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_market_data_fallback.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_order_identity.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_order_time_callback.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_project_updater.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_python_api_reference.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_qmt_strategy_browser.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_qmt_strategy_deploy.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_qmt_strategy_web.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_startup_recovery.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_stock_connect.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_stock_connect_browser.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_trade_strategy_callback.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_trading_dates.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_tutorial_reader.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_tx_auto_import.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/trading_calendar.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tx.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/xtconstant.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/xtdata.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/xttrader.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/xttype.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant.egg-info/dependency_links.txt +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant.egg-info/entry_points.txt +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant.egg-info/requires.txt +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant.egg-info/top_level.txt +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/cfquant_pipe_hub.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/qmt_scripts/CFQUANT.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/qmt_scripts/__init__.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/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.51 → cfquant-0.2.52}/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.51 → cfquant-0.2.52}/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.51 → cfquant-0.2.52}/requirements.txt +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/setup.cfg +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/__init__.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/api-tester.js +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/avatars/amber-pulse.svg +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/avatars/copper-grid.svg +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/avatars/market-blue.svg +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/avatars/rose-circuit.svg +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/avatars/signal-green.svg +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/avatars/slate-wave.svg +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/avatars/teal-orbit.svg +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/avatars/violet-node.svg +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/cftrader-api-data.js +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/docs.js +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/favicon.svg +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/python-api-data.js +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/python-api.js +0 -0
- {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/vendor/mermaid.min.js +0 -0
|
@@ -7,7 +7,8 @@ import time
|
|
|
7
7
|
|
|
8
8
|
from .config import get_config
|
|
9
9
|
from .callback_dispatcher import CallbackDispatcher, dispatch_callbacks
|
|
10
|
-
from .protocol import decode_value, dumps_message, loads_message, new_id, pack_request
|
|
10
|
+
from .protocol import decode_value, dumps_message, loads_message, new_id, pack_request
|
|
11
|
+
from .rpc_timing import RpcTiming
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
class CfquantError(RuntimeError):
|
|
@@ -155,7 +156,7 @@ class LTtxRpcClient(object):
|
|
|
155
156
|
pass
|
|
156
157
|
self._pending.clear()
|
|
157
158
|
|
|
158
|
-
def request(self, action, params=None, timeout=None, request_channel=None):
|
|
159
|
+
def request(self, action, params=None, timeout=None, request_channel=None, response_meta=None):
|
|
159
160
|
effective_timeout = float(timeout or self.timeout)
|
|
160
161
|
request_id = new_id("req")
|
|
161
162
|
raw = pack_request(
|
|
@@ -167,6 +168,7 @@ class LTtxRpcClient(object):
|
|
|
167
168
|
timeout=effective_timeout,
|
|
168
169
|
)
|
|
169
170
|
q = queue.Queue(maxsize=1)
|
|
171
|
+
timing = RpcTiming(response_meta, request_id)
|
|
170
172
|
try:
|
|
171
173
|
with self._lock:
|
|
172
174
|
self.start()
|
|
@@ -177,11 +179,13 @@ class LTtxRpcClient(object):
|
|
|
177
179
|
msg = q.get(timeout=effective_timeout)
|
|
178
180
|
except queue.Empty:
|
|
179
181
|
raise CfquantTimeout("cfquant request timeout: %s" % action)
|
|
180
|
-
|
|
182
|
+
timing.received(msg)
|
|
183
|
+
if not msg.get("ok"):
|
|
181
184
|
err = msg.get("error") or {}
|
|
182
185
|
raise CfquantError(err.get("message") or str(err))
|
|
183
|
-
return
|
|
184
|
-
finally:
|
|
186
|
+
return timing.decode(msg.get("result"), decode_value)
|
|
187
|
+
finally:
|
|
188
|
+
timing.finish()
|
|
185
189
|
with self._pending_lock:
|
|
186
190
|
self._pending.pop(request_id, None)
|
|
187
191
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Level2 data adapters shared by the SDK and QMT bridges."""
|
|
2
2
|
|
|
3
|
+
from .protocol import dataframe_numeric_rows
|
|
4
|
+
|
|
3
5
|
L2_PERIODS = (
|
|
4
6
|
"l2quote", "l2quoteaux", "l2order", "l2transaction",
|
|
5
7
|
"l2transactioncount", "l2orderqueue",
|
|
@@ -93,7 +95,11 @@ def quote_plain(value):
|
|
|
93
95
|
|
|
94
96
|
def quote_records(value):
|
|
95
97
|
if hasattr(value, "columns") and hasattr(value, "to_dict"):
|
|
96
|
-
|
|
98
|
+
rows = dataframe_numeric_rows(value)
|
|
99
|
+
if rows is not None:
|
|
100
|
+
columns = list(value.columns)
|
|
101
|
+
return quote_plain([dict(zip(columns, row)) for row in rows])
|
|
102
|
+
# Keep pandas' scalar boxing for nullable, date-like and object fields.
|
|
97
103
|
return quote_plain(value.to_dict("records"))
|
|
98
104
|
names = getattr(getattr(value, "dtype", None), "names", None)
|
|
99
105
|
if names:
|
|
@@ -184,6 +184,7 @@ class NormalQmtBridge(TxTradeBridge):
|
|
|
184
184
|
msg = loads_message(raw)
|
|
185
185
|
if not msg or msg.get("type") != "request":
|
|
186
186
|
return
|
|
187
|
+
msg["_received_monotonic"] = time.perf_counter()
|
|
187
188
|
action = msg.get("action")
|
|
188
189
|
if action == "cfquant.ping":
|
|
189
190
|
self._send_response(msg, {"pong": True, "ts": time.time(), "request_channel": self.request_channel})
|
|
@@ -840,7 +841,7 @@ class NormalQmtBridge(TxTradeBridge):
|
|
|
840
841
|
client_id = msg.get("client_id") or msg.get("reply_channel")
|
|
841
842
|
if not client_id:
|
|
842
843
|
return
|
|
843
|
-
response = pack_response(msg.get("id"), ok=True, result=result)
|
|
844
|
+
response = pack_response(msg.get("id"), ok=True, result=result, meta=msg.get("_response_meta"))
|
|
844
845
|
self.tx.push("response", response, client_id)
|
|
845
846
|
|
|
846
847
|
def _send_error(self, msg, error):
|
|
@@ -851,7 +852,7 @@ class NormalQmtBridge(TxTradeBridge):
|
|
|
851
852
|
"normal bridge send_error action=%s id=%s client_id=%s error=%s"
|
|
852
853
|
% (msg.get("action"), msg.get("id"), client_id, error)
|
|
853
854
|
)
|
|
854
|
-
response = pack_response(msg.get("id"), ok=False, error=error)
|
|
855
|
+
response = pack_response(msg.get("id"), ok=False, error=error, meta=msg.get("_response_meta"))
|
|
855
856
|
self.tx.push("response", response, client_id)
|
|
856
857
|
|
|
857
858
|
def publish_callback_event(self, event_name, obj):
|
|
@@ -66,7 +66,9 @@ class PipeNormalQmtBridge(NormalQmtBridge):
|
|
|
66
66
|
endpoint_name="normal",
|
|
67
67
|
show=self.show,
|
|
68
68
|
connect_timeout_ms=self.connect_timeout_ms,
|
|
69
|
-
)
|
|
69
|
+
)
|
|
70
|
+
self.tx._log = self._log
|
|
71
|
+
self.tx.start()
|
|
70
72
|
self.tx.start_txg(self.request_channel)
|
|
71
73
|
self.recv_thread = threading.Thread(target=self._recv_loop)
|
|
72
74
|
self.recv_thread.daemon = True
|
|
@@ -153,7 +155,9 @@ class PipeTradeBridge(TxTradeBridge):
|
|
|
153
155
|
endpoint_name="trade",
|
|
154
156
|
show=self.show,
|
|
155
157
|
connect_timeout_ms=self.connect_timeout_ms,
|
|
156
|
-
)
|
|
158
|
+
)
|
|
159
|
+
self.tx._log = self._log
|
|
160
|
+
self.tx.start()
|
|
157
161
|
self.tx.start_txg(self.request_channel)
|
|
158
162
|
self._log(
|
|
159
163
|
"pipe trade bridge started pipe=%s request_channel=%s"
|
|
@@ -11,7 +11,8 @@ from .pipe_transport import (
|
|
|
11
11
|
loads_pipe_message,
|
|
12
12
|
normalize_pipe_name,
|
|
13
13
|
)
|
|
14
|
-
from .protocol import decode_value, dumps_message, loads_message, new_id, pack_request
|
|
14
|
+
from .protocol import decode_value, dumps_message, loads_message, new_id, pack_request
|
|
15
|
+
from .rpc_timing import RpcTiming
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
class PipeRpcClient(object):
|
|
@@ -70,7 +71,7 @@ class PipeRpcClient(object):
|
|
|
70
71
|
self._close_conns_locked()
|
|
71
72
|
self._fail_pending("cfquant pipe client closed")
|
|
72
73
|
|
|
73
|
-
def request(self, action, params=None, timeout=None, request_channel=None):
|
|
74
|
+
def request(self, action, params=None, timeout=None, request_channel=None, response_meta=None):
|
|
74
75
|
effective_timeout = float(timeout or self.timeout)
|
|
75
76
|
request_id = new_id("req")
|
|
76
77
|
raw = pack_request(
|
|
@@ -82,6 +83,7 @@ class PipeRpcClient(object):
|
|
|
82
83
|
timeout=effective_timeout,
|
|
83
84
|
)
|
|
84
85
|
q = queue.Queue(maxsize=1)
|
|
86
|
+
timing = RpcTiming(response_meta, request_id)
|
|
85
87
|
try:
|
|
86
88
|
with self._lock:
|
|
87
89
|
self.start()
|
|
@@ -98,13 +100,15 @@ class PipeRpcClient(object):
|
|
|
98
100
|
from .client import CfquantTimeout
|
|
99
101
|
|
|
100
102
|
raise CfquantTimeout("cfquant pipe request timeout: %s" % action)
|
|
101
|
-
|
|
103
|
+
timing.received(msg)
|
|
104
|
+
if not msg.get("ok"):
|
|
102
105
|
err = msg.get("error") or {}
|
|
103
106
|
from .client import CfquantError
|
|
104
107
|
|
|
105
108
|
raise CfquantError(err.get("message") or str(err))
|
|
106
|
-
return
|
|
107
|
-
finally:
|
|
109
|
+
return timing.decode(msg.get("result"), decode_value)
|
|
110
|
+
finally:
|
|
111
|
+
timing.finish()
|
|
108
112
|
with self._pending_lock:
|
|
109
113
|
self._pending.pop(request_id, None)
|
|
110
114
|
|
|
@@ -80,8 +80,8 @@ def loads_message(raw):
|
|
|
80
80
|
raw = raw.decode("utf-8", errors="replace")
|
|
81
81
|
if not isinstance(raw, str):
|
|
82
82
|
return None
|
|
83
|
-
if "|" in raw:
|
|
84
|
-
_, raw = raw.split("|", 1)
|
|
83
|
+
if not raw.startswith(MESSAGE_PREFIX) and "|" in raw:
|
|
84
|
+
_, raw = raw.split("|", 1)
|
|
85
85
|
if not raw.startswith(MESSAGE_PREFIX):
|
|
86
86
|
return None
|
|
87
87
|
try:
|
|
@@ -107,14 +107,20 @@ def pack_request(action, params=None, reply_channel=None, client_id=None, reques
|
|
|
107
107
|
return dumps_message(payload)
|
|
108
108
|
|
|
109
109
|
|
|
110
|
-
def pack_response(request_id, ok=True, result=None, error=None, meta=None):
|
|
111
|
-
|
|
110
|
+
def pack_response(request_id, ok=True, result=None, error=None, meta=None):
|
|
111
|
+
started = time.perf_counter()
|
|
112
|
+
encoded = encode_value(result)
|
|
113
|
+
meta = dict(meta or {})
|
|
114
|
+
timing = meta.get("timing")
|
|
115
|
+
if isinstance(timing, dict) and isinstance(timing.get("qmt"), dict):
|
|
116
|
+
timing["qmt"]["encode_ms"] = round((time.perf_counter() - started) * 1000, 3)
|
|
117
|
+
return dumps_message({
|
|
112
118
|
"type": "response",
|
|
113
119
|
"id": request_id,
|
|
114
120
|
"ok": bool(ok),
|
|
115
|
-
"result":
|
|
121
|
+
"result": encoded,
|
|
116
122
|
"error": encode_error(error),
|
|
117
|
-
"meta": meta
|
|
123
|
+
"meta": meta,
|
|
118
124
|
})
|
|
119
125
|
|
|
120
126
|
|
|
@@ -212,25 +218,60 @@ def _looks_like_series(value):
|
|
|
212
218
|
)
|
|
213
219
|
|
|
214
220
|
|
|
215
|
-
def _clean_cell(value):
|
|
221
|
+
def _clean_cell(value):
|
|
216
222
|
if value is None:
|
|
217
223
|
return None
|
|
218
224
|
if isinstance(value, float) and (math.isnan(value) or math.isinf(value)):
|
|
219
225
|
return None
|
|
220
|
-
return encode_value(value)
|
|
221
|
-
|
|
222
|
-
|
|
226
|
+
return encode_value(value)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def dataframe_numeric_rows(value):
|
|
230
|
+
"""Bulk rows for plain numeric frames; None keeps pandas' other semantics."""
|
|
231
|
+
columns = getattr(value, "columns", None)
|
|
232
|
+
dtypes = getattr(value, "dtypes", None)
|
|
233
|
+
if columns is None or not len(columns) or dtypes is None or not getattr(columns, "is_unique", False):
|
|
234
|
+
return None
|
|
235
|
+
# Nullable, object and date-like columns need pandas' scalar boxing rules.
|
|
236
|
+
# Restrict this fast path so callers keep their existing null/date behavior.
|
|
237
|
+
for dtype in dtypes:
|
|
238
|
+
if (type(dtype).__module__.split(".", 1)[0] != "numpy"
|
|
239
|
+
or dtype.kind not in ("b", "i", "u", "f") or dtype.itemsize > 8):
|
|
240
|
+
return None
|
|
241
|
+
# Combining mixed numeric blocks before this cast can round large IDs.
|
|
242
|
+
return value.astype(object, copy=False).values.tolist()
|
|
243
|
+
|
|
244
|
+
|
|
223
245
|
def _encode_dataframe(value):
|
|
224
246
|
try:
|
|
225
|
-
|
|
247
|
+
# itertuples builds a Series per column, which is expensive for QMT's
|
|
248
|
+
# stock-by-date history frames. Cast BEFORE combining the blocks: using
|
|
249
|
+
# values directly can round int64/uint64 IDs in mixed float/int frames.
|
|
250
|
+
# astype also works with QMT's pandas 0.22 (which has no to_numpy).
|
|
251
|
+
raw_rows = value.astype(object, copy=False).values.tolist() if len(value.columns) else []
|
|
226
252
|
except Exception:
|
|
227
253
|
try:
|
|
228
|
-
raw_rows = value.
|
|
254
|
+
raw_rows = list(value.itertuples(index=False, name=None))
|
|
229
255
|
except Exception:
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
256
|
+
try:
|
|
257
|
+
raw_rows = value.values.tolist()
|
|
258
|
+
except Exception:
|
|
259
|
+
raw_rows = []
|
|
260
|
+
rows = []
|
|
261
|
+
for row in raw_rows:
|
|
262
|
+
rows.append([_clean_cell(v) for v in row])
|
|
263
|
+
# Thousands of date columns usually share one dtype. Formatting its name
|
|
264
|
+
# repeatedly can cost more than extracting the actual values.
|
|
265
|
+
dtype_flags = {}
|
|
266
|
+
object_columns = []
|
|
267
|
+
for name, dtype in getattr(value, "dtypes", {}).items():
|
|
268
|
+
is_object = dtype_flags.get(dtype)
|
|
269
|
+
if is_object is None:
|
|
270
|
+
dtype_name = str(dtype)
|
|
271
|
+
is_object = dtype_name == "object" or dtype_name.startswith(("Int", "UInt"))
|
|
272
|
+
dtype_flags[dtype] = is_object
|
|
273
|
+
if is_object:
|
|
274
|
+
object_columns.append(_encode_label(name))
|
|
234
275
|
index_name = getattr(getattr(value, "index", None), "name", None)
|
|
235
276
|
return {
|
|
236
277
|
"__cf_type__": "dataframe",
|
|
@@ -238,8 +279,7 @@ def _encode_dataframe(value):
|
|
|
238
279
|
"index": [_encode_label(i) for i in getattr(value, "index", [])],
|
|
239
280
|
"data": rows,
|
|
240
281
|
"index_name": _encode_label(index_name) if index_name is not None else None,
|
|
241
|
-
"object_columns":
|
|
242
|
-
if str(dtype) == "object" or str(dtype).startswith(("Int", "UInt"))],
|
|
282
|
+
"object_columns": object_columns,
|
|
243
283
|
}
|
|
244
284
|
|
|
245
285
|
|
|
@@ -328,6 +368,13 @@ def decode_value(value):
|
|
|
328
368
|
parts[2] = tuple(parts[2])
|
|
329
369
|
dtype_descr.append(tuple(parts))
|
|
330
370
|
dtype = np.dtype(dtype_descr)
|
|
371
|
+
# JSON turns each structured record tuple into a list. NumPy
|
|
372
|
+
# treats that list as another array dimension unless restored.
|
|
373
|
+
def record_tuples(items, dimensions):
|
|
374
|
+
if dimensions == 0:
|
|
375
|
+
return tuple(items)
|
|
376
|
+
return [record_tuples(item, dimensions - 1) for item in items]
|
|
377
|
+
data = record_tuples(data, len(value.get("shape", [len(data)])))
|
|
331
378
|
elif value.get("dtype"):
|
|
332
379
|
try:
|
|
333
380
|
dtype = np.dtype(value.get("dtype"))
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Persisted per-binding QMT scheduling and log preferences."""
|
|
2
|
+
DEFAULT_TIMER_INTERVAL_MS = 1
|
|
3
|
+
MIN_TIMER_INTERVAL_MS = 1
|
|
4
|
+
MAX_TIMER_INTERVAL_MS = 1000
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def normalize_qmt_runtime_settings(value=None, existing=None, strict=False):
|
|
8
|
+
if value is not None and not isinstance(value, dict):
|
|
9
|
+
if strict:
|
|
10
|
+
raise ValueError("QMT runtime settings must be an object")
|
|
11
|
+
value = None
|
|
12
|
+
data = dict(existing or {})
|
|
13
|
+
data.update(value or {})
|
|
14
|
+
interval = data.get("timer_interval_ms", DEFAULT_TIMER_INTERVAL_MS)
|
|
15
|
+
try:
|
|
16
|
+
if isinstance(interval, bool) or str(interval).strip() != str(int(interval)):
|
|
17
|
+
raise ValueError()
|
|
18
|
+
interval = int(interval)
|
|
19
|
+
if not MIN_TIMER_INTERVAL_MS <= interval <= MAX_TIMER_INTERVAL_MS:
|
|
20
|
+
raise ValueError()
|
|
21
|
+
except (TypeError, ValueError, OverflowError):
|
|
22
|
+
if strict:
|
|
23
|
+
raise ValueError("QMT 调度间隔必须是 1–1000 ms 的整数")
|
|
24
|
+
interval = DEFAULT_TIMER_INTERVAL_MS
|
|
25
|
+
enabled = data.get("log_enabled") # None preserves legacy global preferences.
|
|
26
|
+
if enabled is not None and not isinstance(enabled, bool):
|
|
27
|
+
if strict:
|
|
28
|
+
raise ValueError("QMT 日志设置必须为 true、false 或 null(跟随全局)")
|
|
29
|
+
enabled = None
|
|
30
|
+
return {"timer_interval_ms": interval, "log_enabled": enabled}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def qmt_runtime_identity(row, global_log_enabled=True):
|
|
34
|
+
settings = normalize_qmt_runtime_settings((row or {}).get("qmt_runtime"))
|
|
35
|
+
enabled = settings["log_enabled"]
|
|
36
|
+
if (row or {}).get("mode") == "lttx":
|
|
37
|
+
enabled = None # Advanced entries keep their existing global controls.
|
|
38
|
+
return {"qmt_timer_interval_ms": settings["timer_interval_ms"],
|
|
39
|
+
"qmt_log_enabled": bool(global_log_enabled) if enabled is None else enabled}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Per-request diagnostics; never change the RPC method's return value."""
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class RpcTiming(object):
|
|
6
|
+
def __init__(self, output, request_id):
|
|
7
|
+
self.output = output
|
|
8
|
+
self.started = time.perf_counter()
|
|
9
|
+
self.received_at = None
|
|
10
|
+
if output is not None:
|
|
11
|
+
output.update({
|
|
12
|
+
"request_id": request_id,
|
|
13
|
+
"qmt": {"available": False, "reason": "no_response"},
|
|
14
|
+
"rpc": {"response_received": False},
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
def received(self, message):
|
|
18
|
+
self.received_at = time.perf_counter()
|
|
19
|
+
if self.output is None:
|
|
20
|
+
return
|
|
21
|
+
self.output["rpc"].update({
|
|
22
|
+
"response_received": True,
|
|
23
|
+
"response_ok": bool(message.get("ok")),
|
|
24
|
+
"wait_ms": round((self.received_at - self.started) * 1000, 3),
|
|
25
|
+
})
|
|
26
|
+
meta = message.get("meta") or {}
|
|
27
|
+
qmt = (meta.get("timing") or {}).get("qmt")
|
|
28
|
+
self.output["qmt"] = dict(qmt) if isinstance(qmt, dict) else {
|
|
29
|
+
"available": False, "reason": "bridge_timing_unavailable",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
def decode(self, value, decoder):
|
|
33
|
+
started = time.perf_counter()
|
|
34
|
+
try:
|
|
35
|
+
return decoder(value)
|
|
36
|
+
finally:
|
|
37
|
+
if self.output is not None:
|
|
38
|
+
self.output["rpc"]["decode_ms"] = round((time.perf_counter() - started) * 1000, 3)
|
|
39
|
+
|
|
40
|
+
def finish(self):
|
|
41
|
+
if self.output is not None:
|
|
42
|
+
elapsed = round((time.perf_counter() - self.started) * 1000, 3)
|
|
43
|
+
self.output["rpc"]["total_ms"] = elapsed
|
|
44
|
+
if self.received_at is None:
|
|
45
|
+
self.output["rpc"]["wait_ms"] = elapsed
|
|
@@ -32,7 +32,8 @@ CODE = "000001.SZ"
|
|
|
32
32
|
def lite_namespace(path):
|
|
33
33
|
tree = ast.parse(path.read_text(encoding="gbk"))
|
|
34
34
|
helpers = {key for key in vars(level2) if not key.startswith("__") and key != "l2_array"}
|
|
35
|
-
functions = {"encode_value", "_encode_dataframe", "_encode_series", "
|
|
35
|
+
functions = {"encode_value", "_encode_dataframe", "_encode_series", "_encode_label", "_encode_ndarray",
|
|
36
|
+
"_clean_cell", "_looks_like_dataframe", "_looks_like_series", "_get_pandas"}
|
|
36
37
|
body = []
|
|
37
38
|
for node in tree.body:
|
|
38
39
|
if isinstance(node, ast.FunctionDef) and node.name in helpers | functions:
|
|
@@ -143,7 +144,8 @@ def test_sdk_l2_queries_preserve_fields_precision_and_depth(monkeypatch, bridge,
|
|
|
143
144
|
def test_six_period_query_protocol_and_no_forward_fill(bridge, period):
|
|
144
145
|
result = rpc(bridge, "xtdata.get_market_data_ex", {"stock_list": [CODE], "period": period, "fill_data": True})
|
|
145
146
|
assert result[CODE]["orderNo"].iloc[0] == BIG_ID
|
|
146
|
-
assert result[CODE]["bidPrice"].iloc[0]
|
|
147
|
+
assert isinstance(result[CODE]["bidPrice"].iloc[0], np.ndarray)
|
|
148
|
+
np.testing.assert_array_equal(result[CODE]["bidPrice"].iloc[0], [11.6, 11.5])
|
|
147
149
|
assert bridge.context.calls[-1][2] == period
|
|
148
150
|
assert bridge.context.calls[-1][-1] is False
|
|
149
151
|
rpc(bridge, "xtdata.get_market_data", {"stock_list": [CODE], "period": period})
|
|
@@ -254,7 +256,8 @@ def test_wire_encoder_preserves_nullable_ids_arrays_and_numpy_scalars(path):
|
|
|
254
256
|
result = decode_value(json.loads(json.dumps(encoder(frame))))
|
|
255
257
|
assert result["id"].iloc[0] == BIG_ID
|
|
256
258
|
assert result["id"].iloc[1] is None
|
|
257
|
-
assert result["depth"].iloc[0]
|
|
259
|
+
assert isinstance(result["depth"].iloc[0], np.ndarray)
|
|
260
|
+
np.testing.assert_array_equal(result["depth"].iloc[0], [100, 200])
|
|
258
261
|
assert encoder(np.uint64(2 ** 64 - 1)) == 2 ** 64 - 1
|
|
259
262
|
|
|
260
263
|
|
|
@@ -16,8 +16,8 @@ def open_test(page, frontend_url, entry):
|
|
|
16
16
|
requests, errors = open_app(page, frontend_url)
|
|
17
17
|
page.locator('#setupOverlay [data-open-tutorial]').click()
|
|
18
18
|
page.evaluate('(id) => window.CfquantPythonReference.open(id)', entry)
|
|
19
|
-
page.locator('[data-python-test]').click()
|
|
20
|
-
panel = page.locator('.api-inline-test:not([hidden])')
|
|
19
|
+
page.locator('#tutorialReader [data-python-test]').click()
|
|
20
|
+
panel = page.locator('#tutorialReader .api-inline-test:not([hidden])')
|
|
21
21
|
expect(panel).to_be_visible()
|
|
22
22
|
assert all(method == 'GET' for method, path in requests)
|
|
23
23
|
return panel, requests, errors
|
|
@@ -36,7 +36,7 @@ def test_tutorial_query_results_copy_reset_and_error(page, frontend_url):
|
|
|
36
36
|
def reply(route):
|
|
37
37
|
sent.append(route.request.post_data_json)
|
|
38
38
|
route.fulfill(json={'ok': True, 'data': {'result': {'000001.SZ': {'lastPrice': 11.7}}, 'latency_ms': 12.5}})
|
|
39
|
-
page.route('**/api/
|
|
39
|
+
page.route('**/api/python/xtdata', reply)
|
|
40
40
|
panel.locator('[name="code_list"]').fill('000001.SZ')
|
|
41
41
|
panel.locator('[type="submit"]').click()
|
|
42
42
|
expect(panel.locator('[data-test-status]')).to_have_text('请求完成')
|
|
@@ -51,7 +51,7 @@ def test_tutorial_query_results_copy_reset_and_error(page, frontend_url):
|
|
|
51
51
|
expect(panel.locator('[data-test-output]')).to_have_text('')
|
|
52
52
|
panel.locator('[data-test-reset]').click()
|
|
53
53
|
expect(panel.locator('[name="code_list"]')).to_have_value('000001.SZ,600000.SH')
|
|
54
|
-
page.route('**/api/
|
|
54
|
+
page.route('**/api/python/xtdata', lambda route: route.fulfill(status=401, json={'ok': False, 'error': 'invalid credentials'}))
|
|
55
55
|
panel.locator('[type="submit"]').click()
|
|
56
56
|
expect(panel.locator('[data-test-status]')).to_have_text('请求失败')
|
|
57
57
|
expect(panel.locator('[data-test-output]')).to_contain_text('invalid credentials')
|
|
@@ -72,7 +72,7 @@ def test_inline_api_tester_prefers_api_key_when_web_auth_enabled(page, frontend_
|
|
|
72
72
|
sent_headers.append(route.request.headers)
|
|
73
73
|
route.fulfill(json={'ok': True, 'data': {'result': {'000001.SZ': {'lastPrice': 11.7}}}})
|
|
74
74
|
|
|
75
|
-
page.route('**/api/
|
|
75
|
+
page.route('**/api/python/xtdata', reply)
|
|
76
76
|
panel.locator('[name="code_list"]').fill('000001.SZ')
|
|
77
77
|
panel.locator('[type="submit"]').click()
|
|
78
78
|
expect(panel.locator('[data-test-output]')).to_contain_text('lastPrice')
|
|
@@ -179,8 +179,8 @@ def test_stop_waiting_does_not_resubmit_and_result_survives_navigation(page, fro
|
|
|
179
179
|
pending[0].abort()
|
|
180
180
|
page.evaluate("window.CfquantPythonReference.open('xtdata.get_full_tick')")
|
|
181
181
|
page.evaluate("window.CfquantPythonReference.open('cftrader.order_stock_batch')")
|
|
182
|
-
page.locator('[data-python-test]').click()
|
|
183
|
-
expect(page.locator('[data-test-output]')).to_contain_text('避免重复提交')
|
|
182
|
+
page.locator('#tutorialReader [data-python-test]').click()
|
|
183
|
+
expect(page.locator('#tutorialReader [data-test-output]')).to_contain_text('避免重复提交')
|
|
184
184
|
assert len(pending) == 1
|
|
185
185
|
assert errors == []
|
|
186
186
|
|
|
@@ -197,7 +197,7 @@ def test_callback_view_filters_original_events_and_types_are_not_executable(page
|
|
|
197
197
|
expect(panel.locator('[data-test-output]')).to_contain_text('1001')
|
|
198
198
|
assert 'event_name=trader%3Aon_stock_order' in pending[0]
|
|
199
199
|
page.evaluate("window.CfquantPythonReference.open('type.XtAsset')")
|
|
200
|
-
expect(page.locator('[data-python-test]')).to_have_count(0)
|
|
200
|
+
expect(page.locator('#tutorialReader [data-python-test]')).to_have_count(0)
|
|
201
201
|
assert errors == []
|
|
202
202
|
|
|
203
203
|
|
|
@@ -207,6 +207,9 @@ def test_main_api_batch_testing_summary_and_clear(page, frontend_url):
|
|
|
207
207
|
page.mouse.move(page.viewport_size['width'] - 1, page.viewport_size['height'] - 1)
|
|
208
208
|
page.keyboard.press('Escape')
|
|
209
209
|
page.locator('.nav-item[data-view="api"]').click()
|
|
210
|
+
group = page.locator('.api-group').filter(has=page.locator('[data-endpoint-id="cftrader.order_stock_batch"]'))
|
|
211
|
+
if group.locator('.api-group-head').get_attribute('aria-expanded') != 'true':
|
|
212
|
+
group.locator('.api-group-head').click()
|
|
210
213
|
page.locator('[data-endpoint-id="cftrader.order_stock_batch"]').click()
|
|
211
214
|
sent = []
|
|
212
215
|
|
|
@@ -234,3 +237,95 @@ def test_main_api_batch_testing_summary_and_clear(page, frontend_url):
|
|
|
234
237
|
expect(page.locator('#apiResponseBox')).to_have_text('')
|
|
235
238
|
expect(page.locator('#apiResultSummary')).to_have_text('')
|
|
236
239
|
assert errors == []
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def test_main_market_timing_success_timeout_and_old_bridge(page, frontend_url):
|
|
243
|
+
requests, errors = open_app(page, frontend_url, setup_required=False)
|
|
244
|
+
page.locator('#closeOnboardingBtn').click()
|
|
245
|
+
page.mouse.move(page.viewport_size['width'] - 1, page.viewport_size['height'] - 1)
|
|
246
|
+
page.keyboard.press('Escape')
|
|
247
|
+
page.locator('.nav-item[data-view="api"]').click()
|
|
248
|
+
page.locator('[data-api-group="data"]').click()
|
|
249
|
+
page.locator('[data-endpoint-id="full_tick"]').click()
|
|
250
|
+
replies = [{
|
|
251
|
+
'ok': True, 'data': {'latency_ms': 500, 'result': {'000001.SZ': {'lastPrice': 11.3}},
|
|
252
|
+
'timing': {'qmt': {'available': True, 'queue_ms': 400, 'native_ms': 70,
|
|
253
|
+
'convert_ms': 5, 'encode_ms': 4, 'fallback_used': True},
|
|
254
|
+
'rpc': {'wait_ms': 495, 'decode_ms': 2}, 'web': {'result_convert_ms': 3}}}},
|
|
255
|
+
{'ok': False, 'error': 'cfquant pipe request timeout', 'status': 400,
|
|
256
|
+
'timing': {'qmt': {'available': False, 'reason': 'no_response'}, 'rpc': {'wait_ms': 12000}}},
|
|
257
|
+
{'ok': True, 'data': {'latency_ms': 80, 'result': {},
|
|
258
|
+
'timing': {'qmt': {'available': False, 'reason': 'bridge_timing_unavailable'}}}},
|
|
259
|
+
]
|
|
260
|
+
page.route('**/api/data/full-tick', lambda route: route.fulfill(json=replies.pop(0)))
|
|
261
|
+
form = page.locator('#apiForm')
|
|
262
|
+
form.locator('[type="submit"]').click()
|
|
263
|
+
timing = page.locator('#apiResponseLatencyMeta')
|
|
264
|
+
expect(timing).to_contain_text('QMT 原生取数 70.0 ms')
|
|
265
|
+
expect(timing).to_contain_text('兼容格式转换 5.00 ms')
|
|
266
|
+
expect(timing).to_contain_text('Web 结果转换 3.00 ms')
|
|
267
|
+
expect(timing).to_contain_text('旧行情接口已回退到扩展接口')
|
|
268
|
+
assert page.locator('.api-workbench').evaluate('node => node.scrollWidth <= node.clientWidth')
|
|
269
|
+
page.locator('#apiResponseLatency').scroll_into_view_if_needed()
|
|
270
|
+
screenshot(page, 'market-timing')
|
|
271
|
+
form.locator('[type="submit"]').click()
|
|
272
|
+
expect(timing).to_contain_text('QMT 耗时未取得(未收到回包)')
|
|
273
|
+
expect(timing).not_to_contain_text('QMT 原生取数')
|
|
274
|
+
form.locator('[type="submit"]').click()
|
|
275
|
+
expect(timing).to_contain_text('QMT 耗时未取得(桥接脚本未提供)')
|
|
276
|
+
assert errors == []
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def test_market_ex_switch_keeps_query_and_displays_response_timing(page, frontend_url):
|
|
280
|
+
requests, errors = open_app(page, frontend_url, setup_required=False)
|
|
281
|
+
page.locator('#closeOnboardingBtn').click()
|
|
282
|
+
page.mouse.move(page.viewport_size['width'] - 1, page.viewport_size['height'] - 1)
|
|
283
|
+
page.keyboard.press('Escape')
|
|
284
|
+
page.locator('.nav-item[data-view="api"]').click()
|
|
285
|
+
page.locator('[data-api-group="data"]').click()
|
|
286
|
+
entry = page.locator('[data-endpoint-id="market_data_ex"]')
|
|
287
|
+
expect(entry).to_contain_text('get_market_data_ex')
|
|
288
|
+
page.locator('[data-endpoint-id="market_data"]').click()
|
|
289
|
+
form = page.locator('#apiForm')
|
|
290
|
+
fields = {'field_list': 'open,close,volume', 'stock_list': '000001.SZ,600000.SH',
|
|
291
|
+
'start_time': '20260101', 'end_time': '20261014', 'count': '20', 'timeout': '15'}
|
|
292
|
+
for name, value in fields.items():
|
|
293
|
+
form.locator('[name="%s"]' % name).fill(value)
|
|
294
|
+
form.locator('[name="period"]').fill('1d')
|
|
295
|
+
form.locator('[name="fill_data"]').fill('0')
|
|
296
|
+
page.locator('#apiResponseBox').evaluate("node => node.textContent = 'old response'")
|
|
297
|
+
before = page.evaluate('currentApiRequest().body')
|
|
298
|
+
form.locator('[data-market-variant="market_data_ex"]').click()
|
|
299
|
+
expect(page.locator('#apiTitle')).to_contain_text('get_market_data_ex')
|
|
300
|
+
expect(page.locator('#apiRoute')).to_have_text('POST /api/data/market-ex')
|
|
301
|
+
expect(page.locator('#apiResponseBox')).to_have_text('')
|
|
302
|
+
assert page.evaluate('currentApiRequest().body') == before
|
|
303
|
+
pending = []
|
|
304
|
+
page.route('**/api/data/market-ex', lambda route: pending.append(route))
|
|
305
|
+
form.locator('[type="submit"]').click()
|
|
306
|
+
expect(form.locator('[type="submit"]')).to_be_disabled()
|
|
307
|
+
expect(form.locator('[data-market-variant]')).to_be_disabled()
|
|
308
|
+
page.wait_for_function("state.apiDebugBusy")
|
|
309
|
+
assert len(pending) == 1
|
|
310
|
+
assert pending[0].request.post_data_json == before
|
|
311
|
+
assert before['stock_list'] == ['000001.SZ', '600000.SH']
|
|
312
|
+
assert before['field_list'] == ['open', 'close', 'volume']
|
|
313
|
+
assert before['count'] == 20 and before['fill_data'] is False
|
|
314
|
+
pending[0].fulfill(json={'ok': True, 'data': {'latency_ms': 85, 'result': {
|
|
315
|
+
'000001.SZ': {'20261014': {'open': 11.0, 'close': 11.5, 'volume': 120000}}},
|
|
316
|
+
'timing': {'qmt': {'available': True, 'queue_ms': 20, 'native_ms': 5,
|
|
317
|
+
'convert_ms': 0, 'encode_ms': 3, 'fallback_used': False},
|
|
318
|
+
'rpc': {'wait_ms': 80, 'decode_ms': 2}, 'web': {'result_convert_ms': 1}}}})
|
|
319
|
+
expect(page.locator('#apiResponseBox')).to_contain_text('120000')
|
|
320
|
+
expect(page.locator('#apiResponseLatencyMeta')).to_contain_text('QMT 原生取数 5.00 ms')
|
|
321
|
+
expect(page.locator('#apiResponseLatencyMeta')).to_contain_text('QMT 结果编码 3.00 ms')
|
|
322
|
+
expect(page.locator('#apiResponseLatencyMeta')).not_to_contain_text('旧行情接口已回退')
|
|
323
|
+
expect(form.locator('[data-market-variant]')).to_be_enabled()
|
|
324
|
+
assert page.locator('.api-workbench').evaluate('node => node.scrollWidth <= node.clientWidth')
|
|
325
|
+
assert page.locator('#apiEndpointList').evaluate('node => node.scrollWidth <= node.clientWidth')
|
|
326
|
+
screenshot(page, 'market-ex')
|
|
327
|
+
form.locator('[data-market-variant="market_data"]').click()
|
|
328
|
+
expect(page.locator('#apiRoute')).to_have_text('POST /api/data/market')
|
|
329
|
+
assert page.evaluate('currentApiRequest().body') == before
|
|
330
|
+
assert all(method == 'GET' for method, path in requests)
|
|
331
|
+
assert errors == []
|