cfquant 0.2.50__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.50 → cfquant-0.2.52}/PKG-INFO +1 -1
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/build_info.py +2 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/client.py +9 -5
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/level2.py +7 -1
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/normal_bridge.py +11 -3
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/order_identity.py +17 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/pipe_bridge.py +18 -4
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/pipe_client.py +9 -5
- {cfquant-0.2.50 → 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/qmt_pipe_runtime.py +166 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/qmt_strategy_deploy.py +144 -29
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/qmt_strategy_runtime.py +20 -11
- cfquant-0.2.52/cfquant/rpc_timing.py +45 -0
- {cfquant-0.2.50 → 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.50 → 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.50 → cfquant-0.2.52}/cfquant/tests/test_lite_deployment_paths.py +18 -9
- 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.52/cfquant/tests/test_qmt_fileless.py +282 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_qmt_strategy_deploy.py +150 -16
- cfquant-0.2.52/cfquant/tests/test_table_conversion.py +122 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_trade_strategy_callback.py +68 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tx_trade_bridge.py +117 -57
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/version.py +2 -2
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/xttype.py +2 -1
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant.egg-info/PKG-INFO +1 -1
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant.egg-info/SOURCES.txt +10 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant_web_server.py +132 -37
- {cfquant-0.2.50 → cfquant-0.2.52}/pyproject.toml +1 -1
- {cfquant-0.2.50 → cfquant-0.2.52}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +279 -382
- {cfquant-0.2.50 → cfquant-0.2.52}/qmt_scripts/CFQUANT_LITE.py +559 -611
- {cfquant-0.2.50 → 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 +228 -371
- {cfquant-0.2.50 → 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 +228 -371
- {cfquant-0.2.50 → 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 +559 -663
- {cfquant-0.2.50 → 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 +559 -663
- {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/app.js +104 -6
- {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/index.html +24 -2
- {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/styles.css +58 -15
- {cfquant-0.2.50 → cfquant-0.2.52}/LICENSE +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/LTtx/__init__.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/LTtx/tx/Config.txt +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/LTtx/tx/LTtx_server.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/LTtx/tx/__init__.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/LTtx/tx/tx.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/MANIFEST.in +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/README.md +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/__init__.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/_editable_install.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/account_routing.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/batch_orders.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/callback_dispatcher.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/cftrader.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/channels.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/cli.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/config.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/hub.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/log_management.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/logging_i18n.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/management.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/order_meta.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/pipe_hub.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/pipe_transport.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/qmt_bridge.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/qmt_strategy_package.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/runtime_report.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/stock_connect.py +0 -0
- {cfquant-0.2.50 → 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.50 → 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.50 → 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.50 → 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.50 → 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.50 → 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.50 → 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.50 → 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.50 → 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.50 → 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.50 → 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.50 → 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.50 → 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.50 → 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.50 → 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.50 → 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.50 → 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.50 → 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.50 → 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.50 → 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.50 → 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.50 → 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.50 → 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.50 → 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.50 → 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.50 → 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.50 → 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.50 → 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.50 → 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.50 → 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.50 → 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.50 → 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.50 → cfquant-0.2.52}/cfquant/tests/_helpers.py +0 -0
- {cfquant-0.2.50 → 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.50 → 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.50 → cfquant-0.2.52}/cfquant/tests/readme.md +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_advanced_parity.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_api_tester_web.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_callback_requests.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_cftrader.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_cftrader_tutorial.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_cftrader_web.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_credit_compat.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_json_serialization.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_lite_account_routing.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_lite_parity.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_local_trading_calendar.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_log_management.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_management.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_management_batch.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_market_data_fallback.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_order_identity.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_order_time_callback.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_project_updater.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_python_api_reference.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_qmt_strategy_browser.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_qmt_strategy_web.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_startup_recovery.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_stock_connect.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_stock_connect_browser.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_trading_dates.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_tutorial_reader.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_tx_auto_import.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/trading_calendar.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tx.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/xtconstant.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/xtdata.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/xttrader.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant.egg-info/dependency_links.txt +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant.egg-info/entry_points.txt +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant.egg-info/requires.txt +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant.egg-info/top_level.txt +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/cfquant_pipe_hub.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/qmt_scripts/CFQUANT.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/qmt_scripts/__init__.py +0 -0
- {cfquant-0.2.50 → 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.50 → 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.50 → 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.50 → cfquant-0.2.52}/requirements.txt +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/setup.cfg +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/__init__.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/api-tester.js +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/avatars/amber-pulse.svg +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/avatars/copper-grid.svg +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/avatars/market-blue.svg +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/avatars/rose-circuit.svg +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/avatars/signal-green.svg +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/avatars/slate-wave.svg +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/avatars/teal-orbit.svg +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/avatars/violet-node.svg +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/cftrader-api-data.js +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/docs.js +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/favicon.svg +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/python-api-data.js +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/python-api.js +0 -0
- {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/vendor/mermaid.min.js +0 -0
|
@@ -29,6 +29,8 @@ def get_git_commit(source_root=None):
|
|
|
29
29
|
override = _valid_commit(os.environ.get("CFQUANT_GIT_COMMIT"))
|
|
30
30
|
if override:
|
|
31
31
|
return override
|
|
32
|
+
if os.environ.get("CFQUANT_QMT_NO_FILE_IO") == "1":
|
|
33
|
+
return ""
|
|
32
34
|
root = Path(source_root or Path(__file__).resolve().parent.parent).resolve()
|
|
33
35
|
try:
|
|
34
36
|
result = subprocess.run(["git", "-C", str(root), "rev-parse", "HEAD"], stdout=subprocess.PIPE,
|
|
@@ -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:
|
|
@@ -10,7 +10,7 @@ import threading
|
|
|
10
10
|
import time
|
|
11
11
|
|
|
12
12
|
from . import order_meta
|
|
13
|
-
from .order_identity import prepare_order_remark, order_remark_key, restore_order_remark
|
|
13
|
+
from .order_identity import prepare_order_remark, order_remark_key, restore_order_remark, restore_strategy_name
|
|
14
14
|
from .level2 import L2_THOUSAND_SUBSCRIPTIONS, quote_callback_data, quote_plain, require_l2_callable, thousand_price
|
|
15
15
|
from .protocol import loads_message, pack_event, pack_response
|
|
16
16
|
from .tx_trade_bridge import TxTradeBridge, relay_sync_order_callback
|
|
@@ -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})
|
|
@@ -191,6 +192,12 @@ class NormalQmtBridge(TxTradeBridge):
|
|
|
191
192
|
if action == "cfquant.status":
|
|
192
193
|
self._send_response(msg, self._status())
|
|
193
194
|
return
|
|
195
|
+
if action == "cfquant.strategy.control":
|
|
196
|
+
try:
|
|
197
|
+
self._send_response(msg, self._dispatch(action, msg.get("params") or {}, msg))
|
|
198
|
+
except Exception as error:
|
|
199
|
+
self._send_error(msg, error)
|
|
200
|
+
return
|
|
194
201
|
if self._try_enqueue_coalesced_request(msg):
|
|
195
202
|
return
|
|
196
203
|
try:
|
|
@@ -834,7 +841,7 @@ class NormalQmtBridge(TxTradeBridge):
|
|
|
834
841
|
client_id = msg.get("client_id") or msg.get("reply_channel")
|
|
835
842
|
if not client_id:
|
|
836
843
|
return
|
|
837
|
-
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"))
|
|
838
845
|
self.tx.push("response", response, client_id)
|
|
839
846
|
|
|
840
847
|
def _send_error(self, msg, error):
|
|
@@ -845,7 +852,7 @@ class NormalQmtBridge(TxTradeBridge):
|
|
|
845
852
|
"normal bridge send_error action=%s id=%s client_id=%s error=%s"
|
|
846
853
|
% (msg.get("action"), msg.get("id"), client_id, error)
|
|
847
854
|
)
|
|
848
|
-
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"))
|
|
849
856
|
self.tx.push("response", response, client_id)
|
|
850
857
|
|
|
851
858
|
def publish_callback_event(self, event_name, obj):
|
|
@@ -894,6 +901,7 @@ class NormalQmtBridge(TxTradeBridge):
|
|
|
894
901
|
relay_sync_order_callback(self.context, data)
|
|
895
902
|
self._handle_async_order_callback(data)
|
|
896
903
|
self._remember_order_callback_fields(data)
|
|
904
|
+
restore_strategy_name(data)
|
|
897
905
|
payload = {
|
|
898
906
|
"type": "event",
|
|
899
907
|
"event": event_name,
|
|
@@ -12,6 +12,23 @@ from collections import OrderedDict
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
_ORDER_REMARK_SUFFIX = re.compile(r"__cfq_([0-9a-f]{32})$")
|
|
15
|
+
_STRATEGY_NAME_SUFFIX = re.compile(r"(?:&&&_cfq_|^cfq_)[0-9a-f]{12}\Z")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def restore_strategy_name(data):
|
|
19
|
+
"""Remove our request token only at the public callback/object boundary.
|
|
20
|
+
|
|
21
|
+
Keep the full strategy until error-context matching has finished. Match
|
|
22
|
+
the generated suffix precisely so user underscores and delimiters survive.
|
|
23
|
+
"""
|
|
24
|
+
if isinstance(data, dict):
|
|
25
|
+
for name in ("strategy_name", "m_strStrategyName", "mstrStrategyName", "strategyName"):
|
|
26
|
+
value = data.get(name)
|
|
27
|
+
if isinstance(value, str):
|
|
28
|
+
match = _STRATEGY_NAME_SUFFIX.search(value)
|
|
29
|
+
if match:
|
|
30
|
+
data[name] = value[:match.start()]
|
|
31
|
+
return data
|
|
15
32
|
|
|
16
33
|
|
|
17
34
|
def original_order_remark(value):
|
|
@@ -8,7 +8,9 @@ from .pipe_transport import DEFAULT_PIPE_NAME, PipeTxClient
|
|
|
8
8
|
from .tx_trade_bridge import TxTradeBridge
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
class PipeNormalQmtBridge(NormalQmtBridge):
|
|
11
|
+
class PipeNormalQmtBridge(NormalQmtBridge):
|
|
12
|
+
file_io_enabled = False
|
|
13
|
+
|
|
12
14
|
def __init__(
|
|
13
15
|
self,
|
|
14
16
|
context,
|
|
@@ -48,6 +50,9 @@ class PipeNormalQmtBridge(NormalQmtBridge):
|
|
|
48
50
|
self.connect_timeout_ms = int(connect_timeout_ms)
|
|
49
51
|
|
|
50
52
|
def start(self):
|
|
53
|
+
runtime = self.globals_dict.get("_managed_runtime")
|
|
54
|
+
if runtime is not None:
|
|
55
|
+
runtime.attach_bridge(self)
|
|
51
56
|
if self.running:
|
|
52
57
|
return self
|
|
53
58
|
self.running = True
|
|
@@ -61,7 +66,9 @@ class PipeNormalQmtBridge(NormalQmtBridge):
|
|
|
61
66
|
endpoint_name="normal",
|
|
62
67
|
show=self.show,
|
|
63
68
|
connect_timeout_ms=self.connect_timeout_ms,
|
|
64
|
-
)
|
|
69
|
+
)
|
|
70
|
+
self.tx._log = self._log
|
|
71
|
+
self.tx.start()
|
|
65
72
|
self.tx.start_txg(self.request_channel)
|
|
66
73
|
self.recv_thread = threading.Thread(target=self._recv_loop)
|
|
67
74
|
self.recv_thread.daemon = True
|
|
@@ -103,7 +110,9 @@ class PipeNormalQmtBridge(NormalQmtBridge):
|
|
|
103
110
|
return data
|
|
104
111
|
|
|
105
112
|
|
|
106
|
-
class PipeTradeBridge(TxTradeBridge):
|
|
113
|
+
class PipeTradeBridge(TxTradeBridge):
|
|
114
|
+
file_io_enabled = False
|
|
115
|
+
|
|
107
116
|
def __init__(
|
|
108
117
|
self,
|
|
109
118
|
context,
|
|
@@ -131,6 +140,9 @@ class PipeTradeBridge(TxTradeBridge):
|
|
|
131
140
|
self.connect_timeout_ms = int(connect_timeout_ms)
|
|
132
141
|
|
|
133
142
|
def start(self):
|
|
143
|
+
runtime = self.globals_dict.get("_managed_runtime")
|
|
144
|
+
if runtime is not None:
|
|
145
|
+
runtime.attach_bridge(self)
|
|
134
146
|
if self.running:
|
|
135
147
|
return self
|
|
136
148
|
self.running = True
|
|
@@ -143,7 +155,9 @@ class PipeTradeBridge(TxTradeBridge):
|
|
|
143
155
|
endpoint_name="trade",
|
|
144
156
|
show=self.show,
|
|
145
157
|
connect_timeout_ms=self.connect_timeout_ms,
|
|
146
|
-
)
|
|
158
|
+
)
|
|
159
|
+
self.tx._log = self._log
|
|
160
|
+
self.tx.start()
|
|
147
161
|
self.tx.start_txg(self.request_channel)
|
|
148
162
|
self._log(
|
|
149
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,166 @@
|
|
|
1
|
+
"""In-memory managed strategy control over the existing Pipe connection (QMT 3.6+)."""
|
|
2
|
+
|
|
3
|
+
import functools as _cq_functools
|
|
4
|
+
import os as _cq_os
|
|
5
|
+
import sys as _cq_sys
|
|
6
|
+
import threading as _cq_threading
|
|
7
|
+
import time as _cq_time
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ManagedPipeRuntime(object):
|
|
11
|
+
def __init__(self, descriptor=None):
|
|
12
|
+
self.descriptor = dict(descriptor or {})
|
|
13
|
+
self.active = False
|
|
14
|
+
self.approved = False
|
|
15
|
+
self.revoked = False
|
|
16
|
+
self.namespace = {}
|
|
17
|
+
self.original_stop = None
|
|
18
|
+
self.failure = ""
|
|
19
|
+
self.calls = 0
|
|
20
|
+
self.bridges_closed = False
|
|
21
|
+
self.attached_bridges = []
|
|
22
|
+
self.mutex = _cq_threading.RLock()
|
|
23
|
+
|
|
24
|
+
def acquire(self):
|
|
25
|
+
if not self.descriptor:
|
|
26
|
+
self.active = True
|
|
27
|
+
return self
|
|
28
|
+
# sys survives strategy namespace reloads inside one QMT process.
|
|
29
|
+
# Across processes the Pipe hub arbitrates ownership of request channels.
|
|
30
|
+
registry = _cq_sys.__dict__.setdefault("_cfquant_pipe_strategies", {})
|
|
31
|
+
lock = _cq_sys.__dict__.setdefault("_cfquant_pipe_strategies_lock", _cq_threading.RLock())
|
|
32
|
+
with lock:
|
|
33
|
+
key = (self.descriptor["runtime_key"], self.descriptor["role"])
|
|
34
|
+
incompatible = any(scope[0] == key[0] and (
|
|
35
|
+
owner.descriptor.get("mode") != self.descriptor["mode"]
|
|
36
|
+
or owner.descriptor.get("generation") != self.descriptor["generation"])
|
|
37
|
+
for scope, owner in registry.items())
|
|
38
|
+
if key in registry or incompatible:
|
|
39
|
+
raise RuntimeError("Another cfquant strategy still owns this QMT account role.")
|
|
40
|
+
registry[key] = self
|
|
41
|
+
self.active = True
|
|
42
|
+
return self
|
|
43
|
+
|
|
44
|
+
def status(self):
|
|
45
|
+
bridges = [self.namespace.get(name) for name in ("_normal_bridge", "_trade_bridge")
|
|
46
|
+
if self.namespace.get(name) is not None]
|
|
47
|
+
ready = bool(bridges) and all(getattr(bridge, "running", False)
|
|
48
|
+
and getattr(bridge, "context", None) is not None for bridge in bridges)
|
|
49
|
+
state = "error" if self.failure else ("stopped" if not self.active or self.revoked
|
|
50
|
+
else ("running" if ready and self.approved else "loaded"))
|
|
51
|
+
return dict(self.descriptor, state=state, error=self.failure,
|
|
52
|
+
pid=_cq_os.getpid(), updated_at=_cq_time.time())
|
|
53
|
+
|
|
54
|
+
def control(self, desired):
|
|
55
|
+
if desired.get("runtime_key") != self.descriptor.get("runtime_key"):
|
|
56
|
+
raise ValueError("Managed strategy account scope mismatch")
|
|
57
|
+
accepted = desired.get("accepted_generations") or []
|
|
58
|
+
if not isinstance(accepted, (tuple, list)):
|
|
59
|
+
accepted = []
|
|
60
|
+
allowed = (desired.get("enabled") is True
|
|
61
|
+
and desired.get("mode") == self.descriptor["mode"]
|
|
62
|
+
and self.descriptor["generation"] in [desired.get("generation")] + accepted)
|
|
63
|
+
with self.mutex:
|
|
64
|
+
self.approved = allowed and self.active and not self.revoked
|
|
65
|
+
if not allowed and not self.revoked:
|
|
66
|
+
self.revoked = True
|
|
67
|
+
# Block requests immediately; let the control response leave before closing Pipe.
|
|
68
|
+
worker = _cq_threading.Timer(0.1, self.close)
|
|
69
|
+
worker.daemon = True
|
|
70
|
+
worker.start()
|
|
71
|
+
return self.status()
|
|
72
|
+
|
|
73
|
+
def dispatch(self, function):
|
|
74
|
+
dispatch_request = self.wrap(function, reject=True)
|
|
75
|
+
@_cq_functools.wraps(function)
|
|
76
|
+
def guarded(action, params, msg):
|
|
77
|
+
if action == "cfquant.strategy.control":
|
|
78
|
+
return self.control(params or {})
|
|
79
|
+
if action == "cfquant.status":
|
|
80
|
+
return function(action, params, msg)
|
|
81
|
+
if not self.approved:
|
|
82
|
+
raise RuntimeError("Managed strategy is awaiting account mode confirmation from cfquant Web.")
|
|
83
|
+
return dispatch_request(action, params, msg)
|
|
84
|
+
return guarded
|
|
85
|
+
|
|
86
|
+
def wrap(self, function, reject=False):
|
|
87
|
+
@_cq_functools.wraps(function)
|
|
88
|
+
def guarded(*args, **kwargs):
|
|
89
|
+
with self.mutex:
|
|
90
|
+
if not self.active or self.revoked:
|
|
91
|
+
if reject:
|
|
92
|
+
raise RuntimeError("The account's cfquant strategy mode has changed.")
|
|
93
|
+
return None
|
|
94
|
+
self.calls += 1
|
|
95
|
+
try:
|
|
96
|
+
return function(*args, **kwargs)
|
|
97
|
+
except Exception as error:
|
|
98
|
+
if function.__name__ == "init":
|
|
99
|
+
self.failure = str(error)
|
|
100
|
+
self.close()
|
|
101
|
+
raise
|
|
102
|
+
finally:
|
|
103
|
+
with self.mutex:
|
|
104
|
+
self.calls -= 1
|
|
105
|
+
self.release_if_idle()
|
|
106
|
+
return guarded
|
|
107
|
+
|
|
108
|
+
def bind(self, namespace):
|
|
109
|
+
if not self.descriptor:
|
|
110
|
+
return
|
|
111
|
+
self.namespace = namespace
|
|
112
|
+
self.original_stop = namespace.get("stop")
|
|
113
|
+
for name, value in list(namespace.items()):
|
|
114
|
+
if (name in ("init", "after_init", "handlebar", "cfquant_normal_timer", "cfquant_ctype_trade_timer")
|
|
115
|
+
or name.endswith("_callback")) and callable(value):
|
|
116
|
+
namespace[name] = self.wrap(value)
|
|
117
|
+
for name in ("_normal_bridge", "_trade_bridge"):
|
|
118
|
+
bridge = namespace.get(name)
|
|
119
|
+
if bridge is not None:
|
|
120
|
+
self.attach_bridge(bridge)
|
|
121
|
+
namespace["stop"] = self.close
|
|
122
|
+
|
|
123
|
+
def attach_bridge(self, bridge):
|
|
124
|
+
if bridge not in self.attached_bridges:
|
|
125
|
+
self.attached_bridges.append(bridge)
|
|
126
|
+
if not self.descriptor or getattr(bridge, "managed_runtime", None) is self:
|
|
127
|
+
return
|
|
128
|
+
bridge.managed_runtime = self
|
|
129
|
+
bridge._dispatch = self.dispatch(bridge._dispatch)
|
|
130
|
+
for method in ("start", "poll", "pump"):
|
|
131
|
+
function = getattr(bridge, method, None)
|
|
132
|
+
if callable(function):
|
|
133
|
+
setattr(bridge, method, self.wrap(function))
|
|
134
|
+
|
|
135
|
+
def release_if_idle(self):
|
|
136
|
+
if self.bridges_closed and self.calls == 0:
|
|
137
|
+
registry = _cq_sys.__dict__.get("_cfquant_pipe_strategies", {})
|
|
138
|
+
key = (self.descriptor.get("runtime_key"), self.descriptor.get("role"))
|
|
139
|
+
lock = _cq_sys.__dict__.setdefault("_cfquant_pipe_strategies_lock", _cq_threading.RLock())
|
|
140
|
+
with lock:
|
|
141
|
+
if registry.get(key) is self:
|
|
142
|
+
registry.pop(key, None)
|
|
143
|
+
|
|
144
|
+
def close(self, context=None):
|
|
145
|
+
with self.mutex:
|
|
146
|
+
if not self.active:
|
|
147
|
+
return
|
|
148
|
+
self.active = False
|
|
149
|
+
self.approved = False
|
|
150
|
+
try:
|
|
151
|
+
if self.original_stop is not None:
|
|
152
|
+
self.original_stop(context)
|
|
153
|
+
else:
|
|
154
|
+
bridges = list(self.attached_bridges)
|
|
155
|
+
for name in ("_normal_bridge", "_trade_bridge"):
|
|
156
|
+
bridge = self.namespace.get(name)
|
|
157
|
+
if bridge is not None and bridge not in bridges:
|
|
158
|
+
bridges.append(bridge)
|
|
159
|
+
for bridge in bridges:
|
|
160
|
+
bridge.close()
|
|
161
|
+
with self.mutex:
|
|
162
|
+
self.bridges_closed = True
|
|
163
|
+
self.release_if_idle()
|
|
164
|
+
except Exception as error:
|
|
165
|
+
self.failure = str(error)
|
|
166
|
+
print("cfquant strategy shutdown failed: %s" % error)
|