cfquant 0.2.50__tar.gz → 0.2.51__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.51}/PKG-INFO +1 -1
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/build_info.py +2 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/normal_bridge.py +8 -1
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/order_identity.py +17 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/pipe_bridge.py +12 -2
- cfquant-0.2.51/cfquant/qmt_pipe_runtime.py +166 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/qmt_strategy_deploy.py +144 -29
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/qmt_strategy_runtime.py +20 -11
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_lite_deployment_paths.py +18 -9
- cfquant-0.2.51/cfquant/tests/test_qmt_fileless.py +271 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_qmt_strategy_deploy.py +150 -16
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_trade_strategy_callback.py +68 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tx_trade_bridge.py +26 -13
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/version.py +2 -2
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/xttype.py +2 -1
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant.egg-info/PKG-INFO +1 -1
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant.egg-info/SOURCES.txt +2 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant_web_server.py +23 -9
- {cfquant-0.2.50 → cfquant-0.2.51}/pyproject.toml +1 -1
- {cfquant-0.2.50 → cfquant-0.2.51}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +248 -379
- {cfquant-0.2.50 → cfquant-0.2.51}/qmt_scripts/CFQUANT_LITE.py +264 -552
- {cfquant-0.2.50 → cfquant-0.2.51}/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 +196 -367
- {cfquant-0.2.50 → cfquant-0.2.51}/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 +196 -367
- {cfquant-0.2.50 → cfquant-0.2.51}/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 +264 -604
- {cfquant-0.2.50 → cfquant-0.2.51}/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 +264 -604
- {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/app.js +1 -1
- {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/index.html +2 -2
- {cfquant-0.2.50 → cfquant-0.2.51}/LICENSE +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/LTtx/__init__.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/LTtx/tx/Config.txt +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/LTtx/tx/LTtx_server.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/LTtx/tx/__init__.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/LTtx/tx/tx.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/MANIFEST.in +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/README.md +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/__init__.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/_editable_install.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/account_routing.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/batch_orders.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/callback_dispatcher.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/cftrader.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/channels.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/cli.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/client.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/config.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/hub.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/level2.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/log_management.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/logging_i18n.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/management.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/order_meta.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/pipe_client.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/pipe_hub.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/pipe_transport.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/protocol.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/qmt_bridge.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/qmt_strategy_package.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/runtime_report.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/stock_connect.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/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.51}/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.51}/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.51}/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.51}/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.51}/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.51}/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.51}/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.51}/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.51}/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.51}/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.51}/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.51}/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.51}/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.51}/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.51}/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.51}/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.51}/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.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 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/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.50 → cfquant-0.2.51}/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.51}/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.51}/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.51}/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.51}/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.51}/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.51}/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.51}/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.51}/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.51}/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.51}/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.51}/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.51}/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.51}/cfquant/tests/_helpers.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/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.51}/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.51}/cfquant/tests/readme.md +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_advanced_parity.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_api_tester_browser.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_api_tester_web.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_callback_requests.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_cftrader.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_cftrader_tutorial.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_cftrader_web.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_credit_compat.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_json_serialization.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_lite_account_routing.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_lite_parity.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_local_trading_calendar.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_log_management.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_management.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_management_batch.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_market_data_fallback.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_order_identity.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_order_time_callback.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_project_updater.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_python_api_reference.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_qmt_strategy_browser.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_qmt_strategy_web.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_startup_recovery.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_stock_connect.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_stock_connect_browser.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_trading_dates.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_tutorial_reader.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_tx_auto_import.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/trading_calendar.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tx.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/xtconstant.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/xtdata.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/xttrader.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant.egg-info/dependency_links.txt +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant.egg-info/entry_points.txt +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant.egg-info/requires.txt +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant.egg-info/top_level.txt +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/cfquant_pipe_hub.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/qmt_scripts/CFQUANT.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/qmt_scripts/__init__.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/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.51}/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.51}/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.51}/requirements.txt +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/setup.cfg +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/__init__.py +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/api-tester.js +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/avatars/amber-pulse.svg +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/avatars/copper-grid.svg +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/avatars/market-blue.svg +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/avatars/rose-circuit.svg +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/avatars/signal-green.svg +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/avatars/slate-wave.svg +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/avatars/teal-orbit.svg +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/avatars/violet-node.svg +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/cftrader-api-data.js +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/docs.js +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/favicon.svg +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/python-api-data.js +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/python-api.js +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/styles.css +0 -0
- {cfquant-0.2.50 → cfquant-0.2.51}/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,
|
|
@@ -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
|
|
@@ -191,6 +191,12 @@ class NormalQmtBridge(TxTradeBridge):
|
|
|
191
191
|
if action == "cfquant.status":
|
|
192
192
|
self._send_response(msg, self._status())
|
|
193
193
|
return
|
|
194
|
+
if action == "cfquant.strategy.control":
|
|
195
|
+
try:
|
|
196
|
+
self._send_response(msg, self._dispatch(action, msg.get("params") or {}, msg))
|
|
197
|
+
except Exception as error:
|
|
198
|
+
self._send_error(msg, error)
|
|
199
|
+
return
|
|
194
200
|
if self._try_enqueue_coalesced_request(msg):
|
|
195
201
|
return
|
|
196
202
|
try:
|
|
@@ -894,6 +900,7 @@ class NormalQmtBridge(TxTradeBridge):
|
|
|
894
900
|
relay_sync_order_callback(self.context, data)
|
|
895
901
|
self._handle_async_order_callback(data)
|
|
896
902
|
self._remember_order_callback_fields(data)
|
|
903
|
+
restore_strategy_name(data)
|
|
897
904
|
payload = {
|
|
898
905
|
"type": "event",
|
|
899
906
|
"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
|
|
@@ -103,7 +108,9 @@ class PipeNormalQmtBridge(NormalQmtBridge):
|
|
|
103
108
|
return data
|
|
104
109
|
|
|
105
110
|
|
|
106
|
-
class PipeTradeBridge(TxTradeBridge):
|
|
111
|
+
class PipeTradeBridge(TxTradeBridge):
|
|
112
|
+
file_io_enabled = False
|
|
113
|
+
|
|
107
114
|
def __init__(
|
|
108
115
|
self,
|
|
109
116
|
context,
|
|
@@ -131,6 +138,9 @@ class PipeTradeBridge(TxTradeBridge):
|
|
|
131
138
|
self.connect_timeout_ms = int(connect_timeout_ms)
|
|
132
139
|
|
|
133
140
|
def start(self):
|
|
141
|
+
runtime = self.globals_dict.get("_managed_runtime")
|
|
142
|
+
if runtime is not None:
|
|
143
|
+
runtime.attach_bridge(self)
|
|
134
144
|
if self.running:
|
|
135
145
|
return self
|
|
136
146
|
self.running = True
|
|
@@ -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)
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
4
|
import copy
|
|
5
|
-
import hashlib
|
|
6
|
-
import
|
|
5
|
+
import hashlib
|
|
6
|
+
import io
|
|
7
|
+
import json
|
|
7
8
|
import os
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
import re
|
|
@@ -319,14 +320,36 @@ def _resolve_account_binding(root, document, account_id, account_type, supplied=
|
|
|
319
320
|
return None
|
|
320
321
|
|
|
321
322
|
|
|
323
|
+
def _ascii_entry_source(source):
|
|
324
|
+
"""Preserve executable source through QMT's possible Windows ANSI recoding."""
|
|
325
|
+
tokens = []
|
|
326
|
+
for token in tokenize.generate_tokens(io.StringIO(source).readline):
|
|
327
|
+
if not token.string.isascii():
|
|
328
|
+
if token.type == tokenize.STRING:
|
|
329
|
+
# Escape string values, not the whole program. In particular,
|
|
330
|
+
# raw strings must retain their literal backslashes.
|
|
331
|
+
prefix = "u" if token.string[:1].lower() == "u" else ""
|
|
332
|
+
token = token._replace(string=prefix + ascii(ast.literal_eval(token.string)))
|
|
333
|
+
elif token.type == tokenize.COMMENT:
|
|
334
|
+
token = token._replace(string=token.string.encode("ascii", "backslashreplace").decode("ascii"))
|
|
335
|
+
tokens.append(token)
|
|
336
|
+
return tokenize.untokenize(tokens)
|
|
337
|
+
|
|
338
|
+
|
|
322
339
|
def managed_source(source_path, identity_path, identity, descriptor):
|
|
323
340
|
source = read_entry_source(source_path)
|
|
341
|
+
fileless = normalize_strategy_mode(descriptor["mode"]) in ("ctypes", "lite")
|
|
324
342
|
lines = source.splitlines(keepends=True)
|
|
325
343
|
tree = ast.parse(source)
|
|
326
344
|
replacements = []
|
|
327
345
|
constants = {"DEFAULT_ACCOUNT_ID": identity["account_id"], "DEFAULT_ACCOUNT_TYPE": identity["account_type"],
|
|
328
346
|
"USER_BRIDGE_ID": identity["bridge_id"], "BRIDGE_ID": identity["bridge_id"],
|
|
329
|
-
"QMT_MARKET": identity.get("market", "")}
|
|
347
|
+
"QMT_MARKET": identity.get("market", "")}
|
|
348
|
+
if fileless:
|
|
349
|
+
config = dict(identity)
|
|
350
|
+
config["managed_strategy"] = {key: descriptor[key] for key in ("mode", "generation", "role")}
|
|
351
|
+
config["managed_strategy"]["runtime_key"] = descriptor.get("runtime_key") or _digest(str(identity_path))[:24]
|
|
352
|
+
constants["EMBEDDED_RUNTIME_CONFIG"] = config
|
|
330
353
|
seen = set()
|
|
331
354
|
for node in tree.body:
|
|
332
355
|
if isinstance(node, ast.Assign) and len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
|
|
@@ -334,7 +357,7 @@ def managed_source(source_path, identity_path, identity, descriptor):
|
|
|
334
357
|
if name in constants and (name not in seen or name == "QMT_MARKET"):
|
|
335
358
|
replacements.append((node.lineno - 1, node.end_lineno, name + " = " + ascii(constants[name]) + "\n"))
|
|
336
359
|
seen.add(name)
|
|
337
|
-
elif isinstance(node, ast.FunctionDef) and node.name == "_runtime_config_paths":
|
|
360
|
+
elif not fileless and isinstance(node, ast.FunctionDef) and node.name == "_runtime_config_paths":
|
|
338
361
|
replacements.append((node.lineno - 1, node.end_lineno,
|
|
339
362
|
"def _runtime_config_paths():\n return [%s]\n" % ascii(str(identity_path))))
|
|
340
363
|
elif isinstance(node, ast.FunctionDef) and node.name == "_env_allows_runtime_override":
|
|
@@ -345,36 +368,84 @@ def managed_source(source_path, identity_path, identity, descriptor):
|
|
|
345
368
|
lines[0] = "# coding: utf-8\n"
|
|
346
369
|
source = "".join(lines)
|
|
347
370
|
ast.parse(source, feature_version=(3, 6))
|
|
371
|
+
if fileless:
|
|
372
|
+
if "EMBEDDED_RUNTIME_CONFIG" not in seen:
|
|
373
|
+
raise ValueError("QMT Pipe entry does not support embedded account configuration; update the entry source.")
|
|
374
|
+
# Deploy the entry itself. No file lease, exec wrapper, or surrounding try block.
|
|
375
|
+
generated = _ascii_entry_source(source)
|
|
376
|
+
generated.encode("ascii")
|
|
377
|
+
ast.parse(generated, feature_version=(3, 6))
|
|
378
|
+
return generated
|
|
348
379
|
helper = Path(__file__).with_name("qmt_strategy_runtime.py").read_text(encoding="utf-8")
|
|
349
380
|
source_lines = source.splitlines(keepends=True)
|
|
350
|
-
# Adjacent literals compile to the same source string, while keeping every
|
|
351
|
-
# entry line visible in QMT's editor. A single ascii(source) hid thousands
|
|
352
|
-
# of Lite lines behind the same short lease wrapper as the other modes.
|
|
353
381
|
embedded = "\n".join(" " + ascii(line) for line in source_lines)
|
|
382
|
+
entry = " exec(compile((\n%s\n ), %s, 'exec'), globals())\n" % (embedded, ascii(str(source_path)))
|
|
383
|
+
description = "# Embedded entry: %d lines, preserved line by line below.\n" % len(source_lines)
|
|
354
384
|
header = ("# coding: utf-8\n# cfquant managed strategy\n"
|
|
355
385
|
+ "# Mode: " + ascii(descriptor["mode"]) + "\n"
|
|
356
386
|
+ "# Entry: " + ascii(Path(source_path).name) + "\n"
|
|
357
|
-
+
|
|
387
|
+
+ description
|
|
358
388
|
+ "# The lease wrapper is shared by all modes.\n\n")
|
|
359
389
|
generated = (header + helper + "\n_cq_descriptor = " + ascii(descriptor)
|
|
360
390
|
+ "\n_cq_guard = _CqStrategyLease(_cq_descriptor).acquire()\n"
|
|
361
391
|
+ "_cq_guard.namespace = globals()\ntry:\n"
|
|
362
|
-
+
|
|
392
|
+
+ entry
|
|
363
393
|
+ " for _cq_name in ('_cf_bridge', '_normal_bridge', '_trade_bridge'):\n"
|
|
364
394
|
+ " if globals().get(_cq_name) is not None:\n"
|
|
365
395
|
+ " globals()[_cq_name].account_type = %s\n" % ascii(identity["account_type"])
|
|
366
396
|
+ " _cq_guard.bind(globals())\nexcept BaseException as _cq_error:\n"
|
|
367
397
|
+ " _cq_guard.failure = str(_cq_error)\n _cq_guard.close()\n raise\n")
|
|
368
|
-
# QMT may recode source through the Windows ANSI code page before compiling.
|
|
369
|
-
generated.encode("ascii")
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
398
|
+
# QMT may recode source through the Windows ANSI code page before compiling.
|
|
399
|
+
generated.encode("ascii")
|
|
400
|
+
ast.parse(generated, feature_version=(3, 6))
|
|
401
|
+
return generated
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def configured_pipe_source(source, identity):
|
|
405
|
+
"""Embed a standalone entry's config while still in the external deployer."""
|
|
406
|
+
lines = source.splitlines(keepends=True)
|
|
407
|
+
for node in ast.parse(source).body:
|
|
408
|
+
if (isinstance(node, ast.Assign) and len(node.targets) == 1
|
|
409
|
+
and isinstance(node.targets[0], ast.Name) and node.targets[0].id == "EMBEDDED_RUNTIME_CONFIG"):
|
|
410
|
+
lines[node.lineno - 1:node.end_lineno] = ["EMBEDDED_RUNTIME_CONFIG = " + ascii(identity) + "\n"]
|
|
411
|
+
return "".join(lines)
|
|
412
|
+
raise ValueError("QMT Pipe entry is missing EMBEDDED_RUNTIME_CONFIG")
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
class PipeStrategyController:
|
|
416
|
+
"""External Web-side client; QMT receives desired state without opening files."""
|
|
417
|
+
def __init__(self):
|
|
418
|
+
self.clients = {}
|
|
419
|
+
|
|
420
|
+
def __call__(self, identity, desired):
|
|
421
|
+
from .channels import channels_for_bridge
|
|
422
|
+
from .pipe_client import PipeRpcClient
|
|
423
|
+
channel = ((identity.get("channels") or {}).get("normal") or identity.get("normal_channel")
|
|
424
|
+
or channels_for_bridge(identity.get("bridge_id"))["normal"])
|
|
425
|
+
key = (identity.get("pipe_name"), channel)
|
|
426
|
+
client = self.clients.get(key)
|
|
427
|
+
if client is None:
|
|
428
|
+
client = self.clients[key] = PipeRpcClient(pipe_name=key[0], request_channel=channel,
|
|
429
|
+
timeout=0.5, connect_timeout_ms=150)
|
|
430
|
+
try:
|
|
431
|
+
return client.request("cfquant.strategy.control", desired)
|
|
432
|
+
except Exception:
|
|
433
|
+
self.clients.pop(key, None)
|
|
434
|
+
client.close()
|
|
435
|
+
raise
|
|
436
|
+
|
|
437
|
+
def close(self):
|
|
438
|
+
for client in self.clients.values():
|
|
439
|
+
client.close()
|
|
440
|
+
self.clients.clear()
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
class QmtStrategyManager:
|
|
444
|
+
def __init__(self, state_path, scripts_dir, process_checker=qmt_is_running, runtime_request=None):
|
|
375
445
|
self.path = Path(state_path)
|
|
376
446
|
self.scripts_dir = Path(scripts_dir)
|
|
377
|
-
self.process_checker = process_checker
|
|
447
|
+
self.process_checker = process_checker
|
|
448
|
+
self.runtime_request = runtime_request
|
|
378
449
|
self.lock = threading.RLock()
|
|
379
450
|
self.event = threading.Event()
|
|
380
451
|
self.stopping = threading.Event()
|
|
@@ -562,7 +633,7 @@ class QmtStrategyManager:
|
|
|
562
633
|
role.pop("account_key", None)
|
|
563
634
|
fingerprint = _digest([source_group, mode, [
|
|
564
635
|
hashlib.sha256(Path(__file__).with_name(filename).read_bytes()).hexdigest()
|
|
565
|
-
for filename in ("qmt_strategy_runtime.py", "qmt_strategy_package.py", "qmt_strategy_deploy.py")]])
|
|
636
|
+
for filename in ("qmt_strategy_runtime.py", "qmt_pipe_runtime.py", "qmt_strategy_package.py", "qmt_strategy_deploy.py")]])
|
|
566
637
|
if not force and previous.get("fingerprint") == fingerprint and previous.get("enabled"):
|
|
567
638
|
previous["account_key"] = row["account_key"]
|
|
568
639
|
self._prepare_generation_transition(previous)
|
|
@@ -601,11 +672,18 @@ class QmtStrategyManager:
|
|
|
601
672
|
if isinstance(role, dict) and role.get("name")
|
|
602
673
|
)
|
|
603
674
|
retired.difference_update(current_names)
|
|
604
|
-
job = dict(group, account_key=row["account_key"], account_id=row["account_id"],
|
|
675
|
+
job = dict(group, account_key=row["account_key"], account_id=row["account_id"],
|
|
605
676
|
account_type=row["account_type"], mode=mode, settings=settings,
|
|
606
677
|
enabled=True, generation=generation, fingerprint=fingerprint,
|
|
607
678
|
control_path=str(directory / "desired.json"),
|
|
608
|
-
retired=sorted(retired))
|
|
679
|
+
retired=sorted(retired))
|
|
680
|
+
current_endpoints = {self._role_endpoint(role) for role in job["roles"]}
|
|
681
|
+
old_endpoints = {}
|
|
682
|
+
for old_role in previous.get("roles", []) + previous.get("retired_roles", []):
|
|
683
|
+
endpoint = self._role_endpoint(old_role)
|
|
684
|
+
if endpoint not in current_endpoints:
|
|
685
|
+
old_endpoints[endpoint] = {"identity": old_role["identity"], "role": old_role["role"]}
|
|
686
|
+
job["retired_roles"] = list(old_endpoints.values())
|
|
609
687
|
for role in job["roles"]:
|
|
610
688
|
role["name"] = role_names[role["role"]]
|
|
611
689
|
# Every new generation needs its own imported container.
|
|
@@ -616,7 +694,8 @@ class QmtStrategyManager:
|
|
|
616
694
|
role["identity_path"] = str(directory / (role["name"] + ".json"))
|
|
617
695
|
_write_json(role["identity_path"], role["identity"])
|
|
618
696
|
role["runtime_status_path"] = str(directory / (role["name"] + ".status.json"))
|
|
619
|
-
descriptor = {"mode": job["mode"], "generation": generation, "role": role["role"],
|
|
697
|
+
descriptor = {"mode": job["mode"], "generation": generation, "role": role["role"],
|
|
698
|
+
"runtime_key": _digest([os.path.normcase(job["root"]), job["account_id"]])[:24],
|
|
620
699
|
"control_path": job["control_path"], "lease_dir": str(directory / "leases"),
|
|
621
700
|
"runtime_status_path": role["runtime_status_path"]}
|
|
622
701
|
source = managed_source(Path(role["source"]), role["identity_path"], role["identity"], descriptor)
|
|
@@ -642,14 +721,45 @@ class QmtStrategyManager:
|
|
|
642
721
|
self.event.set()
|
|
643
722
|
return self.status(row["account_key"])
|
|
644
723
|
|
|
645
|
-
def _control(self, job):
|
|
724
|
+
def _control(self, job):
|
|
725
|
+
_write_json(job["control_path"], self._desired(job))
|
|
726
|
+
|
|
727
|
+
def _desired(self, job):
|
|
646
728
|
generation = str(job.get("control_generation") or job.get("generation") or "")
|
|
647
729
|
payload = {"enabled": bool(job.get("enabled")), "generation": generation, "mode": job.get("mode", "")}
|
|
648
730
|
accepted = [item for item in _generation_values(job.get("accepted_generations"))
|
|
649
731
|
if item and item != generation]
|
|
650
732
|
if accepted:
|
|
651
733
|
payload["accepted_generations"] = accepted
|
|
652
|
-
|
|
734
|
+
payload["runtime_key"] = _digest([os.path.normcase(job["root"]), job["account_id"]])[:24]
|
|
735
|
+
return payload
|
|
736
|
+
|
|
737
|
+
def _sync_pipe_runtime(self, job):
|
|
738
|
+
if self.runtime_request is None:
|
|
739
|
+
return
|
|
740
|
+
desired = self._desired(job)
|
|
741
|
+
for role in job.get("retired_roles", []):
|
|
742
|
+
try:
|
|
743
|
+
self.runtime_request(role["identity"], dict(desired, enabled=False))
|
|
744
|
+
except Exception:
|
|
745
|
+
pass
|
|
746
|
+
for role in job["roles"]:
|
|
747
|
+
try:
|
|
748
|
+
report = self.runtime_request(role["identity"], desired)
|
|
749
|
+
if (isinstance(report, dict) and report.get("runtime_key") == desired["runtime_key"]
|
|
750
|
+
and report.get("role") == role["role"]):
|
|
751
|
+
# Only the external manager writes status files.
|
|
752
|
+
_write_json(role["runtime_status_path"], report)
|
|
753
|
+
except Exception:
|
|
754
|
+
# A stopped/not-yet-imported strategy has no Pipe endpoint.
|
|
755
|
+
# Existing reports expire normally in _observe_runtime.
|
|
756
|
+
pass
|
|
757
|
+
|
|
758
|
+
@staticmethod
|
|
759
|
+
def _role_endpoint(role):
|
|
760
|
+
identity = role.get("identity") or {}
|
|
761
|
+
return (identity.get("pipe_name"), (identity.get("channels") or {}).get("normal")
|
|
762
|
+
or identity.get("normal_channel") or identity.get("bridge_id"))
|
|
653
763
|
|
|
654
764
|
def reconcile(self, accounts):
|
|
655
765
|
with self.lock:
|
|
@@ -718,9 +828,11 @@ class QmtStrategyManager:
|
|
|
718
828
|
|
|
719
829
|
def _advance(self, job):
|
|
720
830
|
root = qmt_root(job["root"], validate=True)
|
|
721
|
-
if self.process_checker(root):
|
|
722
|
-
self.
|
|
723
|
-
|
|
831
|
+
if self.process_checker(root):
|
|
832
|
+
self._sync_pipe_runtime(job)
|
|
833
|
+
self._observe_runtime(job)
|
|
834
|
+
return
|
|
835
|
+
job["retired_roles"] = []
|
|
724
836
|
if (job.get("state") in ("configured", "running", "waiting_start", "waiting_manual_start")
|
|
725
837
|
and all(role.get("imported") for role in job["roles"])):
|
|
726
838
|
self._state(job, "configured")
|
|
@@ -916,8 +1028,11 @@ class QmtStrategyManager:
|
|
|
916
1028
|
self.event.wait(2.0)
|
|
917
1029
|
self.event.clear()
|
|
918
1030
|
|
|
919
|
-
def close(self):
|
|
1031
|
+
def close(self):
|
|
920
1032
|
self.stopping.set()
|
|
921
1033
|
self.event.set()
|
|
922
|
-
if self.worker:
|
|
923
|
-
self.worker.join(timeout=5.0)
|
|
1034
|
+
if self.worker:
|
|
1035
|
+
self.worker.join(timeout=5.0)
|
|
1036
|
+
close = getattr(self.runtime_request, "close", None)
|
|
1037
|
+
if callable(close) and not (self.worker and self.worker.is_alive()):
|
|
1038
|
+
close()
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import functools as _cq_functools
|
|
4
4
|
import glob as _cq_glob
|
|
5
|
+
import io as _cq_io
|
|
5
6
|
import json as _cq_json
|
|
6
7
|
import os as _cq_os
|
|
7
8
|
import threading as _cq_threading
|
|
@@ -9,7 +10,7 @@ import time as _cq_time
|
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
def _cq_lock(path):
|
|
12
|
-
stream = open(path, "a+b")
|
|
13
|
+
stream = _cq_io.open(path, "a+b")
|
|
13
14
|
try:
|
|
14
15
|
stream.seek(0)
|
|
15
16
|
if _cq_os.name == "nt":
|
|
@@ -62,7 +63,7 @@ class _CqStrategyLease(object):
|
|
|
62
63
|
try:
|
|
63
64
|
payload = {"state": state, "error": error, "generation": self.descriptor["generation"],
|
|
64
65
|
"pid": _cq_os.getpid(), "updated_at": _cq_time.time()}
|
|
65
|
-
with open(temporary, "w", encoding="utf-8") as stream:
|
|
66
|
+
with _cq_io.open(temporary, "w", encoding="utf-8") as stream:
|
|
66
67
|
_cq_json.dump(payload, stream, ensure_ascii=True)
|
|
67
68
|
_cq_os.replace(temporary, path)
|
|
68
69
|
self.last_report = payload["updated_at"]
|
|
@@ -75,10 +76,18 @@ class _CqStrategyLease(object):
|
|
|
75
76
|
except OSError:
|
|
76
77
|
pass
|
|
77
78
|
|
|
79
|
+
def _read_control(self):
|
|
80
|
+
# QMT can supply its own open() in the strategy namespace. Use the
|
|
81
|
+
# standard-library IO API, as the standalone Lite entry already does.
|
|
82
|
+
with _cq_io.open(self.descriptor["control_path"], encoding="utf-8") as stream:
|
|
83
|
+
desired = _cq_json.load(stream)
|
|
84
|
+
if not isinstance(desired, dict):
|
|
85
|
+
raise ValueError("control file must contain a JSON object")
|
|
86
|
+
return desired
|
|
87
|
+
|
|
78
88
|
def allowed(self):
|
|
79
89
|
try:
|
|
80
|
-
|
|
81
|
-
desired = _cq_json.load(stream)
|
|
90
|
+
desired = self._read_control()
|
|
82
91
|
accepted = desired.get("accepted_generations")
|
|
83
92
|
if not isinstance(accepted, (list, tuple, set)):
|
|
84
93
|
accepted = ()
|
|
@@ -87,13 +96,12 @@ class _CqStrategyLease(object):
|
|
|
87
96
|
return (desired.get("enabled") is True
|
|
88
97
|
and self.descriptor["generation"] in allowed_generations
|
|
89
98
|
and desired.get("mode") == self.descriptor["mode"])
|
|
90
|
-
except (OSError, ValueError):
|
|
99
|
+
except (OSError, ValueError, TypeError):
|
|
91
100
|
return False
|
|
92
101
|
|
|
93
102
|
def _denial_reason(self):
|
|
94
103
|
try:
|
|
95
|
-
|
|
96
|
-
desired = _cq_json.load(stream)
|
|
104
|
+
desired = self._read_control()
|
|
97
105
|
if desired.get("enabled") is not True:
|
|
98
106
|
return "control_enabled=false"
|
|
99
107
|
if desired.get("mode") != self.descriptor["mode"]:
|
|
@@ -107,8 +115,9 @@ class _CqStrategyLease(object):
|
|
|
107
115
|
if self.descriptor["generation"] not in allowed_generations:
|
|
108
116
|
return "generation_mismatch expected=%s actual=%s" % (
|
|
109
117
|
desired.get("generation"), self.descriptor["generation"])
|
|
110
|
-
except (OSError, ValueError, TypeError):
|
|
111
|
-
return "control_file_unavailable"
|
|
118
|
+
except (OSError, ValueError, TypeError) as error:
|
|
119
|
+
return "control_file_unavailable path=%s error=%s: %s" % (
|
|
120
|
+
ascii(self.descriptor.get("control_path")), type(error).__name__, str(error))
|
|
112
121
|
return "control_state_changed"
|
|
113
122
|
|
|
114
123
|
def acquire(self, timeout=10.0):
|
|
@@ -126,7 +135,7 @@ class _CqStrategyLease(object):
|
|
|
126
135
|
_cq_unlock(probe)
|
|
127
136
|
continue
|
|
128
137
|
try:
|
|
129
|
-
with open(path + ".json", encoding="utf-8") as stream:
|
|
138
|
+
with _cq_io.open(path + ".json", encoding="utf-8") as stream:
|
|
130
139
|
owner = _cq_json.load(stream)
|
|
131
140
|
compatible = owner.get("generation") == self.descriptor["generation"]
|
|
132
141
|
except (OSError, ValueError):
|
|
@@ -137,7 +146,7 @@ class _CqStrategyLease(object):
|
|
|
137
146
|
if not busy and self.allowed():
|
|
138
147
|
self.lease = _cq_lock(own_path)
|
|
139
148
|
if self.lease is not None:
|
|
140
|
-
with open(own_path + ".json", "w", encoding="utf-8") as stream:
|
|
149
|
+
with _cq_io.open(own_path + ".json", "w", encoding="utf-8") as stream:
|
|
141
150
|
_cq_json.dump(self.descriptor, stream, ensure_ascii=True)
|
|
142
151
|
self.active = True
|
|
143
152
|
return self
|