cfquant 0.2.51__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.51 → cfquant-0.2.53}/PKG-INFO +1 -1
- cfquant-0.2.53/cfquant/callback_dispatcher.py +218 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/client.py +111 -35
- cfquant-0.2.53/cfquant/diagnostics.py +126 -0
- cfquant-0.2.53/cfquant/flow_control.py +171 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/level2.py +7 -1
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/normal_bridge.py +85 -22
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/pipe_bridge.py +8 -4
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/pipe_client.py +38 -14
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/pipe_hub.py +109 -16
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/pipe_transport.py +54 -13
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/protocol.py +147 -25
- cfquant-0.2.53/cfquant/qmt_binding_settings.py +39 -0
- cfquant-0.2.53/cfquant/quote_history.py +122 -0
- cfquant-0.2.53/cfquant/rpc_timing.py +46 -0
- {cfquant-0.2.51 → 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.51 → 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 +6 -3
- 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.51 → cfquant-0.2.53}/cfquant/tests/test_api_tester_browser.py +106 -8
- cfquant-0.2.53/cfquant/tests/test_binding_runtime_browser.py +109 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/tests/test_callback_requests.py +43 -1
- cfquant-0.2.53/cfquant/tests/test_dataframe_encoding.py +192 -0
- cfquant-0.2.53/cfquant/tests/test_latency_flow_control.py +601 -0
- cfquant-0.2.53/cfquant/tests/test_lite_qmt_dispatch.py +348 -0
- cfquant-0.2.53/cfquant/tests/test_market_timing.py +238 -0
- cfquant-0.2.53/cfquant/tests/test_qmt_binding_settings.py +123 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/tests/test_qmt_fileless.py +14 -3
- 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_table_conversion.py +122 -0
- cfquant-0.2.53/cfquant/tests/test_tx_shutdown.py +377 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/tx.py +1942 -1880
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/tx_trade_bridge.py +91 -44
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/version.py +2 -2
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/xtdata.py +16 -5
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/xttrader.py +16 -9
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant.egg-info/PKG-INFO +1 -1
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant.egg-info/SOURCES.txt +16 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant_web_server.py +1140 -850
- {cfquant-0.2.51 → cfquant-0.2.53}/pyproject.toml +1 -1
- {cfquant-0.2.51 → cfquant-0.2.53}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +35 -7
- {cfquant-0.2.51 → cfquant-0.2.53}/qmt_scripts/CFQUANT_LITE.py +605 -97
- {cfquant-0.2.51 → 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 +35 -7
- {cfquant-0.2.51 → 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 +35 -7
- {cfquant-0.2.51 → 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 +605 -97
- {cfquant-0.2.51 → 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 +605 -97
- {cfquant-0.2.51 → cfquant-0.2.53}/web_dashboard/app.js +310 -21
- {cfquant-0.2.51 → cfquant-0.2.53}/web_dashboard/index.html +47 -4
- {cfquant-0.2.51 → cfquant-0.2.53}/web_dashboard/python-api-data.js +2 -2
- {cfquant-0.2.51 → cfquant-0.2.53}/web_dashboard/styles.css +123 -25
- cfquant-0.2.51/cfquant/callback_dispatcher.py +0 -94
- cfquant-0.2.51/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.51 → cfquant-0.2.53}/LICENSE +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/LTtx/__init__.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/LTtx/tx/Config.txt +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/LTtx/tx/LTtx_server.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/LTtx/tx/__init__.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/LTtx/tx/tx.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/MANIFEST.in +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/README.md +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/__init__.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/_editable_install.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/account_routing.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/batch_orders.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/build_info.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/cftrader.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/channels.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/cli.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/config.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/hub.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/log_management.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/logging_i18n.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/management.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/order_identity.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/order_meta.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/qmt_bridge.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/qmt_pipe_runtime.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/qmt_strategy_deploy.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/qmt_strategy_package.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/qmt_strategy_runtime.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/runtime_report.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/stock_connect.py +0 -0
- {cfquant-0.2.51 → 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.51 → 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.51 → 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.51 → 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.51 → 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.51 → 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.51 → 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.51 → 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.51 → 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.51 → 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.51 → 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.51 → 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.51 → 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.51 → 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.51 → 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.51 → 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.51 → 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.51 → 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.51 → 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.51 → 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.51 → 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.51 → 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.51 → 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.51 → 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.51 → 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.51 → 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.51 → 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.51 → 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.51 → 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.51 → 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.51 → cfquant-0.2.53}/cfquant/tests/_helpers.py +0 -0
- {cfquant-0.2.51 → 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.51 → 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.51 → cfquant-0.2.53}/cfquant/tests/readme.md +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/tests/test_advanced_parity.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/tests/test_api_tester_web.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/tests/test_cftrader.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/tests/test_cftrader_tutorial.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/tests/test_cftrader_web.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/tests/test_credit_compat.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/tests/test_json_serialization.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/tests/test_lite_account_routing.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/tests/test_lite_deployment_paths.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/tests/test_lite_parity.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/tests/test_local_trading_calendar.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/tests/test_log_management.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/tests/test_management.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/tests/test_management_batch.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/tests/test_market_data_fallback.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/tests/test_order_identity.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/tests/test_order_time_callback.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/tests/test_project_updater.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/tests/test_python_api_reference.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/tests/test_qmt_strategy_browser.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/tests/test_qmt_strategy_deploy.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/tests/test_qmt_strategy_web.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/tests/test_startup_recovery.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/tests/test_stock_connect.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/tests/test_stock_connect_browser.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/tests/test_trade_strategy_callback.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/tests/test_trading_dates.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/tests/test_tutorial_reader.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/tests/test_tx_auto_import.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/trading_calendar.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/xtconstant.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant/xttype.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant.egg-info/dependency_links.txt +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant.egg-info/entry_points.txt +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant.egg-info/requires.txt +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant.egg-info/top_level.txt +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/cfquant_pipe_hub.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/qmt_scripts/CFQUANT.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/qmt_scripts/__init__.py +0 -0
- {cfquant-0.2.51 → 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.51 → 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.51 → 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.51 → cfquant-0.2.53}/requirements.txt +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/setup.cfg +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/web_dashboard/__init__.py +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/web_dashboard/api-tester.js +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/web_dashboard/avatars/amber-pulse.svg +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/web_dashboard/avatars/copper-grid.svg +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/web_dashboard/avatars/market-blue.svg +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/web_dashboard/avatars/rose-circuit.svg +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/web_dashboard/avatars/signal-green.svg +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/web_dashboard/avatars/slate-wave.svg +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/web_dashboard/avatars/teal-orbit.svg +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/web_dashboard/avatars/violet-node.svg +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/web_dashboard/cftrader-api-data.js +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/web_dashboard/docs.js +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/web_dashboard/favicon.svg +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
- {cfquant-0.2.51 → cfquant-0.2.53}/web_dashboard/python-api.js +0 -0
- {cfquant-0.2.51 → 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
|
|
@@ -7,17 +8,31 @@ import time
|
|
|
7
8
|
|
|
8
9
|
from .config import get_config
|
|
9
10
|
from .callback_dispatcher import CallbackDispatcher, dispatch_callbacks
|
|
10
|
-
from .protocol import decode_value, dumps_message, loads_message, new_id, pack_request
|
|
11
|
+
from .protocol import decode_value, dumps_message, loads_message, new_id, pack_request
|
|
12
|
+
from .rpc_timing import RpcTiming
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
class CfquantError(RuntimeError):
|
|
14
16
|
pass
|
|
15
17
|
|
|
16
18
|
|
|
17
|
-
class CfquantTimeout(TimeoutError):
|
|
18
|
-
pass
|
|
19
|
-
|
|
20
|
-
|
|
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
|
+
|
|
21
36
|
PIPE_TRANSPORTS = ("pipe", "ctypes", "named_pipe", "named-pipe")
|
|
22
37
|
WEB_LTTX_TRANSPORTS = ("web", "web_lttx", "lttx_web", "web-lttx", "lttx-web")
|
|
23
38
|
|
|
@@ -113,9 +128,10 @@ class LTtxRpcClient(object):
|
|
|
113
128
|
self._callbacks = {}
|
|
114
129
|
self._lock = threading.RLock()
|
|
115
130
|
self._pending_lock = threading.RLock()
|
|
116
|
-
self._started = False
|
|
131
|
+
self._started = False
|
|
132
|
+
self._closed = False
|
|
117
133
|
self._recv_thread = None
|
|
118
|
-
self._event_dispatcher = CallbackDispatcher(self._dispatch_event)
|
|
134
|
+
self._event_dispatcher = CallbackDispatcher(self._dispatch_event, self._callback_buffer_failed)
|
|
119
135
|
|
|
120
136
|
def start(self):
|
|
121
137
|
with self._lock:
|
|
@@ -123,19 +139,37 @@ class LTtxRpcClient(object):
|
|
|
123
139
|
if self._started:
|
|
124
140
|
return
|
|
125
141
|
txl = self._load_txl()
|
|
126
|
-
self._tx = txl(self.host, self.port, self.token)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
130
155
|
generation = self._event_dispatcher.start()
|
|
131
156
|
self._recv_thread = threading.Thread(target=self._recv_loop, args=(self._tx, generation))
|
|
132
157
|
self._recv_thread.daemon = True
|
|
133
158
|
self._recv_thread.start()
|
|
134
159
|
|
|
135
|
-
def close(self):
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
139
173
|
tx = self._tx
|
|
140
174
|
self._tx = None
|
|
141
175
|
if tx is not None:
|
|
@@ -143,10 +177,11 @@ class LTtxRpcClient(object):
|
|
|
143
177
|
tx.Q.put(None)
|
|
144
178
|
except Exception:
|
|
145
179
|
pass
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
180
|
+
if tx is not starting_tx:
|
|
181
|
+
try:
|
|
182
|
+
tx.close()
|
|
183
|
+
except Exception:
|
|
184
|
+
pass
|
|
150
185
|
with self._pending_lock:
|
|
151
186
|
for q in list(self._pending.values()):
|
|
152
187
|
try:
|
|
@@ -155,7 +190,7 @@ class LTtxRpcClient(object):
|
|
|
155
190
|
pass
|
|
156
191
|
self._pending.clear()
|
|
157
192
|
|
|
158
|
-
def request(self, action, params=None, timeout=None, request_channel=None):
|
|
193
|
+
def request(self, action, params=None, timeout=None, request_channel=None, response_meta=None):
|
|
159
194
|
effective_timeout = float(timeout or self.timeout)
|
|
160
195
|
request_id = new_id("req")
|
|
161
196
|
raw = pack_request(
|
|
@@ -167,6 +202,7 @@ class LTtxRpcClient(object):
|
|
|
167
202
|
timeout=effective_timeout,
|
|
168
203
|
)
|
|
169
204
|
q = queue.Queue(maxsize=1)
|
|
205
|
+
timing = RpcTiming(response_meta, request_id)
|
|
170
206
|
try:
|
|
171
207
|
with self._lock:
|
|
172
208
|
self.start()
|
|
@@ -174,14 +210,19 @@ class LTtxRpcClient(object):
|
|
|
174
210
|
self._pending[request_id] = q
|
|
175
211
|
self._push("request", raw, request_channel or self.request_channel)
|
|
176
212
|
try:
|
|
177
|
-
msg = q
|
|
213
|
+
msg = _wait_response(q, effective_timeout)
|
|
178
214
|
except queue.Empty:
|
|
179
215
|
raise CfquantTimeout("cfquant request timeout: %s" % action)
|
|
180
|
-
|
|
216
|
+
timing.received(msg)
|
|
217
|
+
if not msg.get("ok"):
|
|
181
218
|
err = msg.get("error") or {}
|
|
182
219
|
raise CfquantError(err.get("message") or str(err))
|
|
183
|
-
return
|
|
184
|
-
|
|
220
|
+
return timing.decode(msg.get("result"), decode_value)
|
|
221
|
+
except (KeyboardInterrupt, SystemExit):
|
|
222
|
+
self.close()
|
|
223
|
+
raise
|
|
224
|
+
finally:
|
|
225
|
+
timing.finish()
|
|
185
226
|
with self._pending_lock:
|
|
186
227
|
self._pending.pop(request_id, None)
|
|
187
228
|
|
|
@@ -209,20 +250,26 @@ class LTtxRpcClient(object):
|
|
|
209
250
|
raw = tx.Q.get()
|
|
210
251
|
if raw is None:
|
|
211
252
|
break
|
|
212
|
-
|
|
253
|
+
parse_started = time.perf_counter()
|
|
254
|
+
msg = loads_message(raw)
|
|
213
255
|
if not msg:
|
|
214
256
|
continue
|
|
215
257
|
with self._lock:
|
|
216
258
|
if not self._started or tx is not self._tx:
|
|
217
259
|
return
|
|
218
260
|
msg_type = msg.get("type")
|
|
219
|
-
if msg_type == "response":
|
|
261
|
+
if msg_type == "response":
|
|
262
|
+
msg["_client_parse_ms"] = round((time.perf_counter() - parse_started) * 1000, 3)
|
|
220
263
|
with self._pending_lock:
|
|
221
264
|
q = self._pending.pop(msg.get("id"), None)
|
|
222
265
|
if q:
|
|
223
266
|
q.put(msg)
|
|
224
|
-
elif msg_type == "event":
|
|
225
|
-
|
|
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
|
|
226
273
|
except Exception:
|
|
227
274
|
time.sleep(0.05)
|
|
228
275
|
if not self._started:
|
|
@@ -231,8 +278,25 @@ class LTtxRpcClient(object):
|
|
|
231
278
|
if self._tx is expected_tx:
|
|
232
279
|
self.close()
|
|
233
280
|
|
|
234
|
-
def _dispatch_event(self, msg):
|
|
235
|
-
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()
|
|
236
300
|
|
|
237
301
|
def _push(self, key, payload, channel):
|
|
238
302
|
tx = self._tx
|
|
@@ -333,7 +397,7 @@ _default_client = None
|
|
|
333
397
|
_client_lock = threading.Lock()
|
|
334
398
|
|
|
335
399
|
|
|
336
|
-
def get_client():
|
|
400
|
+
def get_client():
|
|
337
401
|
global _default_client
|
|
338
402
|
with _client_lock:
|
|
339
403
|
if _default_client is None:
|
|
@@ -343,9 +407,21 @@ def get_client():
|
|
|
343
407
|
timeout=cfg.get("timeout"),
|
|
344
408
|
client_id=cfg.get("client_id"),
|
|
345
409
|
)
|
|
346
|
-
return _default_client
|
|
347
|
-
|
|
348
|
-
|
|
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
|
+
|
|
349
425
|
def configure(**kwargs):
|
|
350
426
|
from .config import configure as configure_config
|
|
351
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)
|