cfquant 0.2.52__tar.gz → 0.2.53__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.52 → cfquant-0.2.53}/PKG-INFO +1 -1
- cfquant-0.2.53/cfquant/callback_dispatcher.py +218 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/client.py +102 -30
- cfquant-0.2.53/cfquant/diagnostics.py +126 -0
- cfquant-0.2.53/cfquant/flow_control.py +171 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/normal_bridge.py +83 -21
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/pipe_bridge.py +2 -2
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/pipe_client.py +29 -9
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/pipe_hub.py +109 -16
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/pipe_transport.py +54 -13
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/protocol.py +86 -11
- cfquant-0.2.53/cfquant/quote_history.py +122 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/rpc_timing.py +1 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/26_/345/205/250/346/216/250/350/256/242/351/230/205/346/265/213/350/257/225.py +149 -1
- cfquant-0.2.53/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 +143 -0
- cfquant-0.2.53/cfquant/tests/34_/345/205/250/346/216/250/345/206/205/351/203/250/345/273/266/350/277/237/346/265/213/350/257/225.py +77 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_api_tester_browser.py +6 -3
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_callback_requests.py +43 -1
- cfquant-0.2.53/cfquant/tests/test_latency_flow_control.py +601 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_lite_qmt_dispatch.py +82 -0
- cfquant-0.2.53/cfquant/tests/test_quote_latency.py +235 -0
- cfquant-0.2.53/cfquant/tests/test_quote_latency_encoding.py +107 -0
- cfquant-0.2.53/cfquant/tests/test_tx_shutdown.py +377 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tx.py +1942 -1880
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/version.py +2 -2
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/xtdata.py +16 -5
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/xttrader.py +16 -9
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant.egg-info/PKG-INFO +1 -1
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant.egg-info/SOURCES.txt +8 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant_web_server.py +1161 -952
- {cfquant-0.2.52 → cfquant-0.2.53}/pyproject.toml +1 -1
- {cfquant-0.2.52 → cfquant-0.2.53}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +2 -2
- {cfquant-0.2.52 → cfquant-0.2.53}/qmt_scripts/CFQUANT_LITE.py +313 -41
- {cfquant-0.2.52 → cfquant-0.2.53}/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 +2 -2
- {cfquant-0.2.52 → cfquant-0.2.53}/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 +2 -2
- {cfquant-0.2.52 → cfquant-0.2.53}/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 +313 -41
- {cfquant-0.2.52 → cfquant-0.2.53}/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 +313 -41
- {cfquant-0.2.52 → cfquant-0.2.53}/web_dashboard/app.js +207 -16
- {cfquant-0.2.52 → cfquant-0.2.53}/web_dashboard/index.html +25 -4
- {cfquant-0.2.52 → cfquant-0.2.53}/web_dashboard/python-api-data.js +2 -2
- {cfquant-0.2.52 → cfquant-0.2.53}/web_dashboard/styles.css +65 -10
- cfquant-0.2.52/cfquant/callback_dispatcher.py +0 -94
- 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 -31
- {cfquant-0.2.52 → cfquant-0.2.53}/LICENSE +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/LTtx/__init__.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/LTtx/tx/Config.txt +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/LTtx/tx/LTtx_server.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/LTtx/tx/__init__.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/LTtx/tx/tx.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/MANIFEST.in +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/README.md +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/__init__.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/_editable_install.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/account_routing.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/batch_orders.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/build_info.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/cftrader.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/channels.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/cli.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/config.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/hub.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/level2.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/log_management.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/logging_i18n.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/management.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/order_identity.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/order_meta.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/qmt_binding_settings.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/qmt_bridge.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/qmt_pipe_runtime.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/qmt_strategy_deploy.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/qmt_strategy_package.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/qmt_strategy_runtime.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/runtime_report.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/stock_connect.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/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.52 → cfquant-0.2.53}/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.52 → cfquant-0.2.53}/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.52 → cfquant-0.2.53}/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.52 → cfquant-0.2.53}/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.52 → cfquant-0.2.53}/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.52 → cfquant-0.2.53}/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.52 → cfquant-0.2.53}/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.52 → cfquant-0.2.53}/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.52 → cfquant-0.2.53}/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.52 → cfquant-0.2.53}/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.52 → cfquant-0.2.53}/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.52 → cfquant-0.2.53}/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.52 → cfquant-0.2.53}/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.52 → cfquant-0.2.53}/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.52 → cfquant-0.2.53}/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.52 → cfquant-0.2.53}/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.52 → cfquant-0.2.53}/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.52 → cfquant-0.2.53}/cfquant/tests/29_Level2/350/241/214/346/203/205/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/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.52 → cfquant-0.2.53}/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.52 → cfquant-0.2.53}/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.52 → cfquant-0.2.53}/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.52 → cfquant-0.2.53}/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.52 → cfquant-0.2.53}/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.52 → cfquant-0.2.53}/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.52 → cfquant-0.2.53}/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.52 → cfquant-0.2.53}/cfquant/tests/6_/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/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.52 → cfquant-0.2.53}/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.52 → cfquant-0.2.53}/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.52 → cfquant-0.2.53}/cfquant/tests/_helpers.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/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.52 → cfquant-0.2.53}/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.52 → cfquant-0.2.53}/cfquant/tests/readme.md +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_advanced_parity.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_api_tester_web.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_binding_runtime_browser.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_cftrader.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_cftrader_tutorial.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_cftrader_web.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_credit_compat.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_dataframe_encoding.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_json_serialization.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_lite_account_routing.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_lite_deployment_paths.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_lite_parity.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_local_trading_calendar.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_log_management.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_management.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_management_batch.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_market_data_fallback.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_market_timing.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_order_identity.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_order_time_callback.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_project_updater.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_python_api_reference.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_qmt_binding_settings.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_qmt_fileless.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_qmt_strategy_browser.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_qmt_strategy_deploy.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_qmt_strategy_web.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_startup_recovery.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_stock_connect.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_stock_connect_browser.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_table_conversion.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_trade_strategy_callback.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_trading_dates.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_tutorial_reader.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tests/test_tx_auto_import.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/trading_calendar.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/tx_trade_bridge.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/xtconstant.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant/xttype.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant.egg-info/dependency_links.txt +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant.egg-info/entry_points.txt +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant.egg-info/requires.txt +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant.egg-info/top_level.txt +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/cfquant_pipe_hub.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/qmt_scripts/CFQUANT.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/qmt_scripts/__init__.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/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.52 → cfquant-0.2.53}/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.52 → cfquant-0.2.53}/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.52 → cfquant-0.2.53}/requirements.txt +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/setup.cfg +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/web_dashboard/__init__.py +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/web_dashboard/api-tester.js +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/web_dashboard/avatars/amber-pulse.svg +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/web_dashboard/avatars/copper-grid.svg +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/web_dashboard/avatars/market-blue.svg +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/web_dashboard/avatars/rose-circuit.svg +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/web_dashboard/avatars/signal-green.svg +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/web_dashboard/avatars/slate-wave.svg +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/web_dashboard/avatars/teal-orbit.svg +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/web_dashboard/avatars/violet-node.svg +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/web_dashboard/cftrader-api-data.js +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/web_dashboard/docs.js +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/web_dashboard/favicon.svg +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/web_dashboard/python-api.js +0 -0
- {cfquant-0.2.52 → cfquant-0.2.53}/web_dashboard/vendor/mermaid.min.js +0 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Serial user callbacks, independent of the RPC receive thread."""
|
|
3
|
+
from collections import deque
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import tempfile
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
from .protocol import decode_value, dumps_message, loads_message
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CallbackDispatcher(object):
|
|
14
|
+
def __init__(self, dispatch, on_error=None):
|
|
15
|
+
self._dispatch = dispatch
|
|
16
|
+
self._on_error = on_error
|
|
17
|
+
self._condition = threading.Condition()
|
|
18
|
+
self._queue = deque()
|
|
19
|
+
self._generation = 0
|
|
20
|
+
self._active = False
|
|
21
|
+
self._thread = None
|
|
22
|
+
self._context = threading.local()
|
|
23
|
+
self._memory_limit = max(0, int(os.environ.get("CFQUANT_CALLBACK_MEMORY_BYTES", 64 * 1024 * 1024)))
|
|
24
|
+
self._memory_bytes = 0
|
|
25
|
+
self._spool = None
|
|
26
|
+
self._spool_size = 0
|
|
27
|
+
self._spool_live = 0
|
|
28
|
+
self._last_wait_ms = self._last_callback_ms = 0.0
|
|
29
|
+
self._last_warning = 0.0
|
|
30
|
+
|
|
31
|
+
def start(self):
|
|
32
|
+
with self._condition:
|
|
33
|
+
self._generation += 1
|
|
34
|
+
self._active = True
|
|
35
|
+
self._clear_queue()
|
|
36
|
+
return self._generation
|
|
37
|
+
|
|
38
|
+
def stop(self):
|
|
39
|
+
with self._condition:
|
|
40
|
+
self._active = False
|
|
41
|
+
self._generation += 1
|
|
42
|
+
self._clear_queue()
|
|
43
|
+
self._condition.notify_all()
|
|
44
|
+
# Never join: close/stop may be called by a callback itself.
|
|
45
|
+
|
|
46
|
+
def submit(self, generation, message, raw=None):
|
|
47
|
+
with self._condition:
|
|
48
|
+
if not self._active or generation != self._generation:
|
|
49
|
+
return
|
|
50
|
+
# Do not block the receive thread on a slow user callback. Trade
|
|
51
|
+
# events cannot be silently dropped or coalesced.
|
|
52
|
+
raw = raw if raw is not None else dumps_message(message)
|
|
53
|
+
size = len(raw) * 8 + 256 # Include the decoded dict/list object overhead.
|
|
54
|
+
self._queue.append([generation, message, time.monotonic(), raw, None, size])
|
|
55
|
+
self._memory_bytes += size
|
|
56
|
+
self._spill_queue()
|
|
57
|
+
if self._thread is None:
|
|
58
|
+
self._thread = threading.Thread(target=self._run, name="cfquant-event-callback")
|
|
59
|
+
self._thread.daemon = True
|
|
60
|
+
self._thread.start()
|
|
61
|
+
self._condition.notify()
|
|
62
|
+
|
|
63
|
+
def _clear_queue(self):
|
|
64
|
+
self._queue.clear()
|
|
65
|
+
self._memory_bytes = self._spool_size = self._spool_live = 0
|
|
66
|
+
if self._spool is not None:
|
|
67
|
+
self._spool.close()
|
|
68
|
+
self._spool = None
|
|
69
|
+
|
|
70
|
+
def _spill_queue(self):
|
|
71
|
+
# Allow one full batch in memory: a single large snapshot is not a
|
|
72
|
+
# backlog and must not make every healthy callback perform disk IO.
|
|
73
|
+
if len(self._queue) <= 1:
|
|
74
|
+
return
|
|
75
|
+
for item in self._queue:
|
|
76
|
+
if self._memory_bytes <= self._memory_limit:
|
|
77
|
+
break
|
|
78
|
+
if item[4] is not None:
|
|
79
|
+
continue
|
|
80
|
+
if self._spool is None:
|
|
81
|
+
self._spool = tempfile.TemporaryFile(prefix="cfquant-callbacks-")
|
|
82
|
+
raw = item[3].encode("utf-8") if isinstance(item[3], str) else item[3]
|
|
83
|
+
self._spool.seek(self._spool_size)
|
|
84
|
+
if self._spool.write(raw) != len(raw):
|
|
85
|
+
raise OSError("callback backlog could not be preserved")
|
|
86
|
+
item[4] = (self._spool_size, len(raw))
|
|
87
|
+
self._spool_size += len(raw)
|
|
88
|
+
self._spool_live += len(raw)
|
|
89
|
+
self._memory_bytes -= item[5]
|
|
90
|
+
item[1] = item[3] = None
|
|
91
|
+
if self._queue and len(self._queue) >= 1000 and time.monotonic() - self._last_warning >= 5:
|
|
92
|
+
self._last_warning = time.monotonic()
|
|
93
|
+
logging.getLogger(__name__).warning("callback backlog count=%s oldest_ms=%.1f spooled_bytes=%s",
|
|
94
|
+
len(self._queue), (time.monotonic() - self._queue[0][2]) * 1000, self._spool_live)
|
|
95
|
+
|
|
96
|
+
def _take_message(self, item):
|
|
97
|
+
if item[4] is None:
|
|
98
|
+
self._memory_bytes -= item[5]
|
|
99
|
+
return item[1], None
|
|
100
|
+
offset, size = item[4]
|
|
101
|
+
self._spool.seek(offset)
|
|
102
|
+
raw = self._spool.read(size)
|
|
103
|
+
if len(raw) != size:
|
|
104
|
+
raise OSError("callback backlog read was incomplete")
|
|
105
|
+
self._spool_live -= size
|
|
106
|
+
if self._spool_live == 0:
|
|
107
|
+
self._spool.close()
|
|
108
|
+
self._spool = None
|
|
109
|
+
self._spool_size = 0
|
|
110
|
+
elif self._spool_size > max(self._memory_limit, 1024 * 1024) + self._spool_live * 2:
|
|
111
|
+
replacement = tempfile.TemporaryFile(prefix="cfquant-callbacks-")
|
|
112
|
+
offsets = []
|
|
113
|
+
try:
|
|
114
|
+
for queued in self._queue:
|
|
115
|
+
if queued[4] is None:
|
|
116
|
+
continue
|
|
117
|
+
previous, length = queued[4]
|
|
118
|
+
self._spool.seek(previous)
|
|
119
|
+
value = self._spool.read(length)
|
|
120
|
+
next_offset = replacement.tell()
|
|
121
|
+
if len(value) != length or replacement.write(value) != length:
|
|
122
|
+
raise OSError("callback backlog compaction was incomplete")
|
|
123
|
+
offsets.append((queued, (next_offset, length)))
|
|
124
|
+
except BaseException:
|
|
125
|
+
replacement.close()
|
|
126
|
+
raise
|
|
127
|
+
self._spool.close()
|
|
128
|
+
self._spool = replacement
|
|
129
|
+
self._spool_size = replacement.tell()
|
|
130
|
+
for queued, location in offsets:
|
|
131
|
+
queued[4] = location
|
|
132
|
+
return None, raw
|
|
133
|
+
|
|
134
|
+
def status(self):
|
|
135
|
+
with self._condition:
|
|
136
|
+
return {"queued_callbacks": len(self._queue), "estimated_memory_bytes": self._memory_bytes,
|
|
137
|
+
"memory_limit_bytes": self._memory_limit, "spooled_bytes": self._spool_live,
|
|
138
|
+
"oldest_age_ms": round((time.monotonic() - self._queue[0][2]) * 1000, 3) if self._queue else 0,
|
|
139
|
+
"last_queue_ms": round(self._last_wait_ms, 3), "last_callback_ms": round(self._last_callback_ms, 3)}
|
|
140
|
+
|
|
141
|
+
def is_current_callback(self):
|
|
142
|
+
generation = getattr(self._context, "generation", None)
|
|
143
|
+
with self._condition:
|
|
144
|
+
return generation is None or (self._active and generation == self._generation)
|
|
145
|
+
|
|
146
|
+
def is_generation(self, generation):
|
|
147
|
+
with self._condition:
|
|
148
|
+
return generation == self._generation
|
|
149
|
+
|
|
150
|
+
def check_request(self):
|
|
151
|
+
if not self.is_current_callback():
|
|
152
|
+
raise RuntimeError("callback belongs to a closed cfquant connection")
|
|
153
|
+
|
|
154
|
+
def _run(self):
|
|
155
|
+
while True:
|
|
156
|
+
try:
|
|
157
|
+
with self._condition:
|
|
158
|
+
while self._active and not self._queue:
|
|
159
|
+
self._condition.wait()
|
|
160
|
+
if not self._active:
|
|
161
|
+
self._thread = None
|
|
162
|
+
return
|
|
163
|
+
item = self._queue.popleft()
|
|
164
|
+
generation = item[0]
|
|
165
|
+
message, raw = self._take_message(item)
|
|
166
|
+
if message is None:
|
|
167
|
+
message = loads_message(raw)
|
|
168
|
+
if message is None:
|
|
169
|
+
raise ValueError("invalid preserved callback message")
|
|
170
|
+
except Exception as error:
|
|
171
|
+
logging.getLogger(__name__).exception("callback backlog failed; connection must be restarted")
|
|
172
|
+
with self._condition:
|
|
173
|
+
if generation != self._generation:
|
|
174
|
+
continue # An old connection must not close its replacement.
|
|
175
|
+
self._active = False
|
|
176
|
+
self._clear_queue()
|
|
177
|
+
self._thread = None
|
|
178
|
+
if self._on_error is not None:
|
|
179
|
+
self._on_error(error, generation)
|
|
180
|
+
return
|
|
181
|
+
started = time.monotonic()
|
|
182
|
+
self._last_wait_ms = (started - item[2]) * 1000
|
|
183
|
+
self._context.generation = generation
|
|
184
|
+
try:
|
|
185
|
+
if self.is_current_callback():
|
|
186
|
+
self._dispatch(message)
|
|
187
|
+
except Exception:
|
|
188
|
+
logging.getLogger(__name__).exception("callback dispatch failed event=%s", message.get("event"))
|
|
189
|
+
finally:
|
|
190
|
+
self._last_callback_ms = (time.monotonic() - started) * 1000
|
|
191
|
+
del self._context.generation
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def dispatch_callbacks(callbacks, message, is_current):
|
|
195
|
+
event = message.get("event")
|
|
196
|
+
raw_data = message.get("data")
|
|
197
|
+
meta = message.get("meta")
|
|
198
|
+
plain_quote = (isinstance(raw_data, dict) and isinstance(meta, dict)
|
|
199
|
+
and meta.get("quote_json_plain") is True)
|
|
200
|
+
# The direct snapshot encoder emits only JSON scalars/containers, no tagged
|
|
201
|
+
# SDK values. Do not walk all symbols a second time after JSON parsing.
|
|
202
|
+
data = raw_data if plain_quote else decode_value(raw_data)
|
|
203
|
+
full_message = dict(message, data=data)
|
|
204
|
+
deliveries = [(list(callbacks.get("__event__", [])), full_message),
|
|
205
|
+
(list(callbacks.get(event, [])), data)]
|
|
206
|
+
if event and event.startswith("quote:"):
|
|
207
|
+
quote_message = dict(full_message)
|
|
208
|
+
if quote_message.get("subscription_id") is not None and quote_message.get("subscribe_id") is None:
|
|
209
|
+
quote_message["subscribe_id"] = quote_message["subscription_id"]
|
|
210
|
+
deliveries.append((list(callbacks.get("quote", [])), quote_message))
|
|
211
|
+
for handlers, payload in deliveries:
|
|
212
|
+
for callback in handlers:
|
|
213
|
+
if not is_current():
|
|
214
|
+
return
|
|
215
|
+
try:
|
|
216
|
+
callback(payload)
|
|
217
|
+
except Exception:
|
|
218
|
+
logging.getLogger(__name__).exception("user callback failed event=%s", event)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
-
import json
|
|
2
|
+
import json
|
|
3
|
+
import atexit
|
|
3
4
|
import queue
|
|
4
5
|
import socket
|
|
5
6
|
import threading
|
|
@@ -15,10 +16,23 @@ class CfquantError(RuntimeError):
|
|
|
15
16
|
pass
|
|
16
17
|
|
|
17
18
|
|
|
18
|
-
class CfquantTimeout(TimeoutError):
|
|
19
|
-
pass
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
class CfquantTimeout(TimeoutError):
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _wait_response(q, timeout):
|
|
24
|
+
"""Keep long Windows waits interruptible without delaying incoming data."""
|
|
25
|
+
if timeout < 0 or threading.current_thread() is not threading.main_thread():
|
|
26
|
+
return q.get(timeout=timeout)
|
|
27
|
+
deadline = time.monotonic() + timeout
|
|
28
|
+
while True:
|
|
29
|
+
try:
|
|
30
|
+
return q.get(timeout=min(.2, max(0, deadline - time.monotonic())))
|
|
31
|
+
except queue.Empty:
|
|
32
|
+
if time.monotonic() >= deadline:
|
|
33
|
+
raise
|
|
34
|
+
|
|
35
|
+
|
|
22
36
|
PIPE_TRANSPORTS = ("pipe", "ctypes", "named_pipe", "named-pipe")
|
|
23
37
|
WEB_LTTX_TRANSPORTS = ("web", "web_lttx", "lttx_web", "web-lttx", "lttx-web")
|
|
24
38
|
|
|
@@ -114,9 +128,10 @@ class LTtxRpcClient(object):
|
|
|
114
128
|
self._callbacks = {}
|
|
115
129
|
self._lock = threading.RLock()
|
|
116
130
|
self._pending_lock = threading.RLock()
|
|
117
|
-
self._started = False
|
|
131
|
+
self._started = False
|
|
132
|
+
self._closed = False
|
|
118
133
|
self._recv_thread = None
|
|
119
|
-
self._event_dispatcher = CallbackDispatcher(self._dispatch_event)
|
|
134
|
+
self._event_dispatcher = CallbackDispatcher(self._dispatch_event, self._callback_buffer_failed)
|
|
120
135
|
|
|
121
136
|
def start(self):
|
|
122
137
|
with self._lock:
|
|
@@ -124,19 +139,37 @@ class LTtxRpcClient(object):
|
|
|
124
139
|
if self._started:
|
|
125
140
|
return
|
|
126
141
|
txl = self._load_txl()
|
|
127
|
-
self._tx = txl(self.host, self.port, self.token)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
142
|
+
self._tx = txl(self.host, self.port, self.token)
|
|
143
|
+
try:
|
|
144
|
+
result = self._tx.start_tx()
|
|
145
|
+
if isinstance(result, dict) and result.get("code", 0) != 0:
|
|
146
|
+
raise CfquantError(result.get("msg") or "LTtx connection failed")
|
|
147
|
+
self._tx.start_txg(self.client_id)
|
|
148
|
+
if not getattr(self._tx, "run_on", True):
|
|
149
|
+
raise CfquantError("cfquant client closed during connection startup")
|
|
150
|
+
except BaseException:
|
|
151
|
+
self.close()
|
|
152
|
+
raise
|
|
153
|
+
self._started = True
|
|
154
|
+
self._closed = False
|
|
131
155
|
generation = self._event_dispatcher.start()
|
|
132
156
|
self._recv_thread = threading.Thread(target=self._recv_loop, args=(self._tx, generation))
|
|
133
157
|
self._recv_thread.daemon = True
|
|
134
158
|
self._recv_thread.start()
|
|
135
159
|
|
|
136
|
-
def close(self):
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
160
|
+
def close(self):
|
|
161
|
+
# start() owns _lock while connecting/retrying. Cancel the underlying
|
|
162
|
+
# transport first so a close from another thread can acquire that lock.
|
|
163
|
+
starting_tx = self._tx if not self._started else None
|
|
164
|
+
if starting_tx is not None:
|
|
165
|
+
try:
|
|
166
|
+
starting_tx.close()
|
|
167
|
+
except Exception:
|
|
168
|
+
pass
|
|
169
|
+
with self._lock:
|
|
170
|
+
self._event_dispatcher.stop()
|
|
171
|
+
self._started = False
|
|
172
|
+
self._closed = True
|
|
140
173
|
tx = self._tx
|
|
141
174
|
self._tx = None
|
|
142
175
|
if tx is not None:
|
|
@@ -144,10 +177,11 @@ class LTtxRpcClient(object):
|
|
|
144
177
|
tx.Q.put(None)
|
|
145
178
|
except Exception:
|
|
146
179
|
pass
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
180
|
+
if tx is not starting_tx:
|
|
181
|
+
try:
|
|
182
|
+
tx.close()
|
|
183
|
+
except Exception:
|
|
184
|
+
pass
|
|
151
185
|
with self._pending_lock:
|
|
152
186
|
for q in list(self._pending.values()):
|
|
153
187
|
try:
|
|
@@ -176,7 +210,7 @@ class LTtxRpcClient(object):
|
|
|
176
210
|
self._pending[request_id] = q
|
|
177
211
|
self._push("request", raw, request_channel or self.request_channel)
|
|
178
212
|
try:
|
|
179
|
-
msg = q
|
|
213
|
+
msg = _wait_response(q, effective_timeout)
|
|
180
214
|
except queue.Empty:
|
|
181
215
|
raise CfquantTimeout("cfquant request timeout: %s" % action)
|
|
182
216
|
timing.received(msg)
|
|
@@ -184,6 +218,9 @@ class LTtxRpcClient(object):
|
|
|
184
218
|
err = msg.get("error") or {}
|
|
185
219
|
raise CfquantError(err.get("message") or str(err))
|
|
186
220
|
return timing.decode(msg.get("result"), decode_value)
|
|
221
|
+
except (KeyboardInterrupt, SystemExit):
|
|
222
|
+
self.close()
|
|
223
|
+
raise
|
|
187
224
|
finally:
|
|
188
225
|
timing.finish()
|
|
189
226
|
with self._pending_lock:
|
|
@@ -213,20 +250,26 @@ class LTtxRpcClient(object):
|
|
|
213
250
|
raw = tx.Q.get()
|
|
214
251
|
if raw is None:
|
|
215
252
|
break
|
|
216
|
-
|
|
253
|
+
parse_started = time.perf_counter()
|
|
254
|
+
msg = loads_message(raw)
|
|
217
255
|
if not msg:
|
|
218
256
|
continue
|
|
219
257
|
with self._lock:
|
|
220
258
|
if not self._started or tx is not self._tx:
|
|
221
259
|
return
|
|
222
260
|
msg_type = msg.get("type")
|
|
223
|
-
if msg_type == "response":
|
|
261
|
+
if msg_type == "response":
|
|
262
|
+
msg["_client_parse_ms"] = round((time.perf_counter() - parse_started) * 1000, 3)
|
|
224
263
|
with self._pending_lock:
|
|
225
264
|
q = self._pending.pop(msg.get("id"), None)
|
|
226
265
|
if q:
|
|
227
266
|
q.put(msg)
|
|
228
|
-
elif msg_type == "event":
|
|
229
|
-
|
|
267
|
+
elif msg_type == "event":
|
|
268
|
+
try:
|
|
269
|
+
self._event_dispatcher.submit(generation, msg, raw=raw)
|
|
270
|
+
except Exception as error:
|
|
271
|
+
self._callback_buffer_failed(error)
|
|
272
|
+
return
|
|
230
273
|
except Exception:
|
|
231
274
|
time.sleep(0.05)
|
|
232
275
|
if not self._started:
|
|
@@ -235,8 +278,25 @@ class LTtxRpcClient(object):
|
|
|
235
278
|
if self._tx is expected_tx:
|
|
236
279
|
self.close()
|
|
237
280
|
|
|
238
|
-
def _dispatch_event(self, msg):
|
|
239
|
-
dispatch_callbacks(self._callbacks, msg, self._event_dispatcher.is_current_callback)
|
|
281
|
+
def _dispatch_event(self, msg):
|
|
282
|
+
dispatch_callbacks(self._callbacks, msg, self._event_dispatcher.is_current_callback)
|
|
283
|
+
|
|
284
|
+
def callback_status(self):
|
|
285
|
+
return self._event_dispatcher.status()
|
|
286
|
+
|
|
287
|
+
def _callback_buffer_failed(self, error, generation=None):
|
|
288
|
+
with self._lock:
|
|
289
|
+
if generation is not None and not self._event_dispatcher.is_generation(generation):
|
|
290
|
+
return
|
|
291
|
+
with self._pending_lock:
|
|
292
|
+
pending = list(self._pending.values())
|
|
293
|
+
self._pending.clear()
|
|
294
|
+
for waiter in pending:
|
|
295
|
+
try:
|
|
296
|
+
waiter.put_nowait({"ok": False, "error": {"message": "cfquant callback buffering failed: %s" % error}})
|
|
297
|
+
except queue.Full:
|
|
298
|
+
pass
|
|
299
|
+
self.close()
|
|
240
300
|
|
|
241
301
|
def _push(self, key, payload, channel):
|
|
242
302
|
tx = self._tx
|
|
@@ -337,7 +397,7 @@ _default_client = None
|
|
|
337
397
|
_client_lock = threading.Lock()
|
|
338
398
|
|
|
339
399
|
|
|
340
|
-
def get_client():
|
|
400
|
+
def get_client():
|
|
341
401
|
global _default_client
|
|
342
402
|
with _client_lock:
|
|
343
403
|
if _default_client is None:
|
|
@@ -347,9 +407,21 @@ def get_client():
|
|
|
347
407
|
timeout=cfg.get("timeout"),
|
|
348
408
|
client_id=cfg.get("client_id"),
|
|
349
409
|
)
|
|
350
|
-
return _default_client
|
|
351
|
-
|
|
352
|
-
|
|
410
|
+
return _default_client
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def close_client():
|
|
414
|
+
"""Close the default market-data client without creating a connection."""
|
|
415
|
+
global _default_client
|
|
416
|
+
with _client_lock:
|
|
417
|
+
client, _default_client = _default_client, None
|
|
418
|
+
if client is not None:
|
|
419
|
+
client.close()
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
atexit.register(close_client)
|
|
423
|
+
|
|
424
|
+
|
|
353
425
|
def configure(**kwargs):
|
|
354
426
|
from .config import configure as configure_config
|
|
355
427
|
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Opt-in whole-quote latency diagnostics; ordinary xtdata payloads are unchanged."""
|
|
3
|
+
import datetime
|
|
4
|
+
import math
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
from . import xtdata
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
SCHEMA = "cfquant.quote_latency.v1"
|
|
11
|
+
ACTION = "xtdata.subscribe_whole_quote_latency"
|
|
12
|
+
_SHANGHAI = datetime.timezone(datetime.timedelta(hours=8))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def quote_time_ms(value):
|
|
16
|
+
"""Accept epoch seconds/ms/us/ns and QMT calendar strings (China time).
|
|
17
|
+
|
|
18
|
+
Time-only and date-only fields are deliberately not assigned today's date.
|
|
19
|
+
"""
|
|
20
|
+
if value is None or isinstance(value, bool):
|
|
21
|
+
return None
|
|
22
|
+
text = str(value).strip()
|
|
23
|
+
if text.isdigit() and len(text) in (14, 17) and text[:2] in ("19", "20"):
|
|
24
|
+
try:
|
|
25
|
+
stamp = datetime.datetime.strptime(text[:14], "%Y%m%d%H%M%S").replace(tzinfo=_SHANGHAI)
|
|
26
|
+
return stamp.timestamp() * 1000.0 + (int(text[14:]) if len(text) == 17 else 0)
|
|
27
|
+
except ValueError:
|
|
28
|
+
return None
|
|
29
|
+
try:
|
|
30
|
+
number = float(value)
|
|
31
|
+
except (ValueError, TypeError, OverflowError):
|
|
32
|
+
for fmt in ("%Y-%m-%d %H:%M:%S.%f", "%Y-%m-%d %H:%M:%S", "%Y%m%d %H:%M:%S.%f", "%Y%m%d %H:%M:%S"):
|
|
33
|
+
try:
|
|
34
|
+
return datetime.datetime.strptime(text, fmt).replace(tzinfo=_SHANGHAI).timestamp() * 1000.0
|
|
35
|
+
except ValueError:
|
|
36
|
+
pass
|
|
37
|
+
return None
|
|
38
|
+
if not math.isfinite(number) or number < 1000000000:
|
|
39
|
+
return None
|
|
40
|
+
if number >= 1e17:
|
|
41
|
+
number /= 1e6
|
|
42
|
+
elif number >= 1e14:
|
|
43
|
+
number /= 1e3
|
|
44
|
+
elif number < 1e11:
|
|
45
|
+
number *= 1000.0
|
|
46
|
+
# Reject short dates and implausible timestamps rather than report bogus latency.
|
|
47
|
+
return number if 946684800000 <= number < 4102444800000 else None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def latency_stats(values):
|
|
51
|
+
values = sorted(value for value in values if value is not None and math.isfinite(value))
|
|
52
|
+
if not values:
|
|
53
|
+
return {"count": 0, "min_ms": None, "max_ms": None, "avg_ms": None, "p95_ms": None}
|
|
54
|
+
return {"count": len(values), "min_ms": values[0], "max_ms": values[-1],
|
|
55
|
+
"avg_ms": round(sum(values) / len(values), 3),
|
|
56
|
+
"p95_ms": values[max(0, (len(values) * 95 + 99) // 100 - 1)]}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def latency_report(payload, received_at_ms=None, sample_limit=None):
|
|
60
|
+
"""Measure at callback entry, before reporting/rendering. Never clamp negative deltas."""
|
|
61
|
+
received_at_ms = time.time() * 1000.0 if received_at_ms is None else received_at_ms
|
|
62
|
+
analysis_started = time.perf_counter()
|
|
63
|
+
if not isinstance(payload, dict) or payload.get("schema") != SCHEMA:
|
|
64
|
+
raise ValueError("not a cfquant whole-quote latency event; update the QMT entry script")
|
|
65
|
+
quotes = payload.get("quotes") or {}
|
|
66
|
+
pushed = quote_time_ms(payload.get("qmt_push_at_ms"))
|
|
67
|
+
entered = quote_time_ms(payload.get("qmt_callback_at_ms"))
|
|
68
|
+
|
|
69
|
+
def delta(end, start):
|
|
70
|
+
return round(end - start, 3) if end is not None and start is not None else None
|
|
71
|
+
|
|
72
|
+
sample_limit = None if sample_limit is None else max(1, int(sample_limit))
|
|
73
|
+
samples = []
|
|
74
|
+
for code, quote in quotes.items():
|
|
75
|
+
if sample_limit is not None and len(samples) >= sample_limit:
|
|
76
|
+
break
|
|
77
|
+
if not isinstance(quote, dict):
|
|
78
|
+
continue
|
|
79
|
+
stamp, field, raw = None, None, None
|
|
80
|
+
for name in ("time", "stime", "timetag", "datetime", "updateTime", "update_time"):
|
|
81
|
+
candidate = quote.get(name)
|
|
82
|
+
if raw is None and candidate is not None:
|
|
83
|
+
field, raw = name, candidate
|
|
84
|
+
stamp = quote_time_ms(candidate)
|
|
85
|
+
if stamp is not None:
|
|
86
|
+
field, raw = name, candidate
|
|
87
|
+
break
|
|
88
|
+
samples.append({
|
|
89
|
+
"code": str(code), "quote_time_field": field, "quote_time_raw": raw,
|
|
90
|
+
"quote_at_ms": stamp, "quote_latency_ms": delta(received_at_ms, stamp),
|
|
91
|
+
# Use the timestamp captured at QMT callback entry, before locking
|
|
92
|
+
# or conversion; receiver/formatting time must not affect this value.
|
|
93
|
+
"qmt_quote_latency_ms": delta(entered, stamp),
|
|
94
|
+
"last_price": quote.get("lastPrice", quote.get("last_price")),
|
|
95
|
+
"volume": quote.get("volume"), "amount": quote.get("amount"),
|
|
96
|
+
})
|
|
97
|
+
summary = {
|
|
98
|
+
"quote_latency": latency_stats(row["quote_latency_ms"] for row in samples),
|
|
99
|
+
"qmt_quote_latency": latency_stats(row["qmt_quote_latency_ms"] for row in samples),
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
"schema": SCHEMA, "received_at_ms": received_at_ms,
|
|
103
|
+
"qmt_callback_at_ms": entered, "qmt_push_at_ms": pushed,
|
|
104
|
+
"internal_latency_ms": delta(received_at_ms, pushed),
|
|
105
|
+
"qmt_prepare_ms": delta(pushed, entered),
|
|
106
|
+
"qmt_encode_ms": payload.get("qmt_encode_ms"), "qmt_encoding": payload.get("qmt_encoding"),
|
|
107
|
+
"code_count": len(quotes), "samples": samples, "summary": summary,
|
|
108
|
+
"analysis_ms": round((time.perf_counter() - analysis_started) * 1000, 3),
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def subscribe_whole_quote(code_list, callback, sample_limit=None):
|
|
113
|
+
"""Independent diagnostic subscription. Callback receives a measured report.
|
|
114
|
+
|
|
115
|
+
Use xtdata.unsubscribe_quote(id) to stop. Internal latency includes encoding,
|
|
116
|
+
transport, decoding and callback scheduling, ending at this Python callback.
|
|
117
|
+
"""
|
|
118
|
+
if not callable(callback):
|
|
119
|
+
raise TypeError("callback must be callable")
|
|
120
|
+
sample_limit = None if sample_limit is None else max(1, int(sample_limit))
|
|
121
|
+
|
|
122
|
+
def on_quote(payload):
|
|
123
|
+
received_at_ms = time.time() * 1000.0
|
|
124
|
+
callback(latency_report(payload, received_at_ms, sample_limit))
|
|
125
|
+
|
|
126
|
+
return xtdata._subscribe_quote_request(ACTION, {"code_list": code_list}, on_quote)
|