cfquant 0.2.47__tar.gz → 0.2.49__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.47 → cfquant-0.2.49}/PKG-INFO +1 -1
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/qmt_strategy_deploy.py +95 -44
- {cfquant-0.2.47 → cfquant-0.2.49}/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 +4 -4
- cfquant-0.2.49/cfquant/tests/test_lite_deployment_paths.py +169 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_qmt_strategy_deploy.py +71 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_qmt_strategy_web.py +4 -3
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/version.py +2 -2
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant.egg-info/PKG-INFO +1 -1
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant.egg-info/SOURCES.txt +1 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant_web_server.py +115 -79
- {cfquant-0.2.47 → cfquant-0.2.49}/pyproject.toml +1 -1
- {cfquant-0.2.47 → cfquant-0.2.49}/qmt_scripts/CFQUANT_LITE.py +1 -1
- {cfquant-0.2.47 → cfquant-0.2.49}/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 +1 -1
- {cfquant-0.2.47 → cfquant-0.2.49}/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 +1 -1
- {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/app.js +1 -1
- {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/index.html +2 -2
- {cfquant-0.2.47 → cfquant-0.2.49}/LICENSE +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/LTtx/__init__.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/LTtx/tx/Config.txt +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/LTtx/tx/LTtx_server.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/LTtx/tx/__init__.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/LTtx/tx/tx.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/MANIFEST.in +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/README.md +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/__init__.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/_editable_install.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/account_routing.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/batch_orders.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/build_info.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/callback_dispatcher.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/cftrader.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/channels.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/cli.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/client.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/config.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/hub.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/level2.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/log_management.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/logging_i18n.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/management.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/normal_bridge.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/order_identity.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/order_meta.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/pipe_bridge.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/pipe_client.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/pipe_hub.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/pipe_transport.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/protocol.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/qmt_bridge.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/qmt_strategy_package.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/qmt_strategy_runtime.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/runtime_report.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/stock_connect.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/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.47 → cfquant-0.2.49}/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.47 → cfquant-0.2.49}/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.47 → cfquant-0.2.49}/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.47 → cfquant-0.2.49}/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.47 → cfquant-0.2.49}/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.47 → cfquant-0.2.49}/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.47 → cfquant-0.2.49}/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.47 → cfquant-0.2.49}/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.47 → cfquant-0.2.49}/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.47 → cfquant-0.2.49}/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.47 → cfquant-0.2.49}/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.47 → cfquant-0.2.49}/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.47 → cfquant-0.2.49}/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.47 → cfquant-0.2.49}/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.47 → cfquant-0.2.49}/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.47 → cfquant-0.2.49}/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.47 → cfquant-0.2.49}/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.47 → cfquant-0.2.49}/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.47 → cfquant-0.2.49}/cfquant/tests/29_Level2/350/241/214/346/203/205/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/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.47 → cfquant-0.2.49}/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.47 → cfquant-0.2.49}/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.47 → cfquant-0.2.49}/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.47 → cfquant-0.2.49}/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.47 → cfquant-0.2.49}/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.47 → cfquant-0.2.49}/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.47 → cfquant-0.2.49}/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.47 → cfquant-0.2.49}/cfquant/tests/6_/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/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.47 → cfquant-0.2.49}/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.47 → cfquant-0.2.49}/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.47 → cfquant-0.2.49}/cfquant/tests/_helpers.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/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.47 → cfquant-0.2.49}/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.47 → cfquant-0.2.49}/cfquant/tests/readme.md +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_api_tester_browser.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_api_tester_web.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_callback_requests.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_cftrader.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_cftrader_tutorial.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_cftrader_web.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_credit_compat.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_json_serialization.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_lite_account_routing.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_local_trading_calendar.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_log_management.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_management.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_management_batch.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_market_data_fallback.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_order_identity.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_order_time_callback.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_project_updater.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_python_api_reference.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_qmt_strategy_browser.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_startup_recovery.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_stock_connect.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_stock_connect_browser.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_trading_dates.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_tutorial_reader.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_tx_auto_import.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/trading_calendar.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tx.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tx_trade_bridge.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/xtconstant.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/xtdata.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/xttrader.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/xttype.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant.egg-info/dependency_links.txt +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant.egg-info/entry_points.txt +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant.egg-info/requires.txt +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant.egg-info/top_level.txt +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/cfquant_pipe_hub.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/qmt_scripts/CFQUANT.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/qmt_scripts/__init__.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/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" +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/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" +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/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.47 → cfquant-0.2.49}/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.47 → cfquant-0.2.49}/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.47 → cfquant-0.2.49}/requirements.txt +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/setup.cfg +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/__init__.py +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/api-tester.js +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/avatars/amber-pulse.svg +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/avatars/copper-grid.svg +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/avatars/market-blue.svg +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/avatars/rose-circuit.svg +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/avatars/signal-green.svg +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/avatars/slate-wave.svg +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/avatars/teal-orbit.svg +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/avatars/violet-node.svg +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/cftrader-api-data.js +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/docs.js +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/favicon.svg +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/python-api-data.js +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/python-api.js +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/styles.css +0 -0
- {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/vendor/mermaid.min.js +0 -0
|
@@ -23,7 +23,10 @@ MODE_ALIASES = {
|
|
|
23
23
|
"ctypes": "ctypes", "pipe": "ctypes", "named_pipe": "ctypes",
|
|
24
24
|
"lite": "lite", "extreme": "lite", "extreme_lite": "lite",
|
|
25
25
|
"lite_extreme": "lite", "ultimate": "lite",
|
|
26
|
+
"lite_extreme_pipe": "lite", "extreme_pipe": "lite", "cfquant_lite": "lite",
|
|
27
|
+
"named-pipe": "ctypes", "universal": "ctypes", "universal_ctypes": "ctypes",
|
|
26
28
|
"lttx": "lttx", "socket": "lttx", "normal": "lttx",
|
|
29
|
+
"default": "lttx",
|
|
27
30
|
}
|
|
28
31
|
LEGACY_NAMES = {Path(name).stem for name in SCRIPTS.values()} | {"CFQUANT_TRADE_LOWLAT"}
|
|
29
32
|
LEGACY_NAMES |= {name + "_" + market for name in tuple(LEGACY_NAMES) for market in ("SH", "SZ")}
|
|
@@ -68,9 +71,42 @@ def normalize_strategy_mode(value):
|
|
|
68
71
|
return MODE_ALIASES[mode]
|
|
69
72
|
except KeyError:
|
|
70
73
|
raise ValueError("unknown QMT strategy mode: %s" % mode)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def entry_script_for_mode(mode, role="normal", market=""):
|
|
77
|
+
"""One mapping for raw copies and the packages QMT actually imports."""
|
|
78
|
+
mode = normalize_strategy_mode(mode)
|
|
79
|
+
market = str(market or "").strip().upper()
|
|
80
|
+
if market and market not in ("SH", "SZ"):
|
|
81
|
+
raise ValueError("unknown QMT entry market: %s" % market)
|
|
82
|
+
filename = SCRIPTS[mode]
|
|
83
|
+
if mode == "lttx" and (role != "normal" or market):
|
|
84
|
+
filename = "CFQUANT_TRADE_LOWLAT.py"
|
|
85
|
+
if market:
|
|
86
|
+
return "同账号独立市场/%s_%s.py" % (Path(filename).stem, market)
|
|
87
|
+
return filename
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def read_entry_source(path):
|
|
91
|
+
"""Reject a generic entry accidentally saved under a Lite filename."""
|
|
92
|
+
path = Path(path)
|
|
93
|
+
with tokenize.open(str(path)) as stream:
|
|
94
|
+
source = stream.read()
|
|
95
|
+
if path.stem.startswith("CFQUANT_LITE"):
|
|
96
|
+
tree = ast.parse(source)
|
|
97
|
+
constants = {target.id for node in tree.body if isinstance(node, ast.Assign)
|
|
98
|
+
for target in node.targets if isinstance(target, ast.Name)}
|
|
99
|
+
imports_core = any(
|
|
100
|
+
(isinstance(node, ast.ImportFrom) and (node.module or "").split(".")[0] == "cfquant")
|
|
101
|
+
or (isinstance(node, ast.Import) and any(alias.name.split(".")[0] == "cfquant"
|
|
102
|
+
for alias in node.names))
|
|
103
|
+
for node in ast.walk(tree))
|
|
104
|
+
if "LITE_ENTRY_VERSION" not in constants or imports_core:
|
|
105
|
+
raise ValueError("极致模式入口内容不匹配,拒绝部署通用模式代码: %s" % path)
|
|
106
|
+
return source
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def qmt_root(directory, validate=False):
|
|
74
110
|
path = Path(str(directory)).expanduser().resolve()
|
|
75
111
|
if path.name.lower() in ("bin.x64", "bin", "python"):
|
|
76
112
|
path = path.parent
|
|
@@ -283,9 +319,8 @@ def _resolve_account_binding(root, document, account_id, account_type, supplied=
|
|
|
283
319
|
return None
|
|
284
320
|
|
|
285
321
|
|
|
286
|
-
def managed_source(source_path, identity_path, identity, descriptor):
|
|
287
|
-
|
|
288
|
-
source = stream.read()
|
|
322
|
+
def managed_source(source_path, identity_path, identity, descriptor):
|
|
323
|
+
source = read_entry_source(source_path)
|
|
289
324
|
lines = source.splitlines(keepends=True)
|
|
290
325
|
tree = ast.parse(source)
|
|
291
326
|
replacements = []
|
|
@@ -308,13 +343,23 @@ def managed_source(source_path, identity_path, identity, descriptor):
|
|
|
308
343
|
for start, end, text in sorted(replacements, reverse=True):
|
|
309
344
|
lines[start:end] = [text]
|
|
310
345
|
lines[0] = "# coding: utf-8\n"
|
|
311
|
-
source = "".join(lines)
|
|
312
|
-
ast.parse(source, feature_version=(3, 6))
|
|
313
|
-
helper = Path(__file__).with_name("qmt_strategy_runtime.py").read_text(encoding="utf-8")
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
346
|
+
source = "".join(lines)
|
|
347
|
+
ast.parse(source, feature_version=(3, 6))
|
|
348
|
+
helper = Path(__file__).with_name("qmt_strategy_runtime.py").read_text(encoding="utf-8")
|
|
349
|
+
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
|
+
embedded = "\n".join(" " + ascii(line) for line in source_lines)
|
|
354
|
+
header = ("# coding: utf-8\n# cfquant managed strategy\n"
|
|
355
|
+
+ "# Mode: " + ascii(descriptor["mode"]) + "\n"
|
|
356
|
+
+ "# Entry: " + ascii(Path(source_path).name) + "\n"
|
|
357
|
+
+ "# Embedded entry: %d lines, preserved line by line below.\n" % len(source_lines)
|
|
358
|
+
+ "# The lease wrapper is shared by all modes.\n\n")
|
|
359
|
+
generated = (header + helper + "\n_cq_descriptor = " + ascii(descriptor)
|
|
360
|
+
+ "\n_cq_guard = _CqStrategyLease(_cq_descriptor).acquire()\n"
|
|
361
|
+
+ "_cq_guard.namespace = globals()\ntry:\n"
|
|
362
|
+
+ " exec(compile((\n%s\n ), %s, 'exec'), globals())\n" % (embedded, ascii(str(source_path)))
|
|
318
363
|
+ " for _cq_name in ('_cf_bridge', '_normal_bridge', '_trade_bridge'):\n"
|
|
319
364
|
+ " if globals().get(_cq_name) is not None:\n"
|
|
320
365
|
+ " globals()[_cq_name].account_type = %s\n" % ascii(identity["account_type"])
|
|
@@ -373,13 +418,17 @@ class QmtStrategyManager:
|
|
|
373
418
|
job.pop("deploy_retry_count", None)
|
|
374
419
|
job.pop("deploy_retry_after", None)
|
|
375
420
|
|
|
376
|
-
def _clear_generation_transition(self, job):
|
|
377
|
-
changed = False
|
|
378
|
-
for key in ("control_generation", "accepted_generations"):
|
|
379
|
-
if key in job:
|
|
380
|
-
job.pop(key, None)
|
|
381
|
-
changed = True
|
|
382
|
-
|
|
421
|
+
def _clear_generation_transition(self, job):
|
|
422
|
+
changed = False
|
|
423
|
+
for key in ("control_generation", "accepted_generations"):
|
|
424
|
+
if key in job:
|
|
425
|
+
job.pop(key, None)
|
|
426
|
+
changed = True
|
|
427
|
+
for role in job.get("roles", []):
|
|
428
|
+
if "reimport_from_generation" in role:
|
|
429
|
+
role.pop("reimport_from_generation", None)
|
|
430
|
+
changed = True
|
|
431
|
+
return changed
|
|
383
432
|
|
|
384
433
|
def _prepare_generation_transition(self, job, previous=None):
|
|
385
434
|
"""Keep old same-name containers valid until the replacement starts."""
|
|
@@ -432,19 +481,23 @@ class QmtStrategyManager:
|
|
|
432
481
|
)
|
|
433
482
|
job["control_generation"] = legacy
|
|
434
483
|
job["accepted_generations"] = accepted
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
484
|
+
for role in job.get("roles", []):
|
|
485
|
+
if (_runtime_generation(role) == legacy
|
|
486
|
+
and role.get("reimport_from_generation") != legacy):
|
|
487
|
+
# The old report survives QMT shutdown and import. Handle it
|
|
488
|
+
# once per replacement job, including across Web restarts;
|
|
489
|
+
# resetting every poll destroys the prepared/imported model.
|
|
490
|
+
updates = {
|
|
491
|
+
"reimport_from_generation": legacy,
|
|
492
|
+
"reimport_required": True,
|
|
493
|
+
"imported": False,
|
|
494
|
+
"queued": False,
|
|
495
|
+
"model_prepared": False,
|
|
496
|
+
}
|
|
497
|
+
for key, value in updates.items():
|
|
498
|
+
if role.get(key) != value:
|
|
499
|
+
role[key] = value
|
|
500
|
+
changed = True
|
|
448
501
|
if changed and job.get("state") == "error":
|
|
449
502
|
self._state(job, "waiting_exit")
|
|
450
503
|
return changed
|
|
@@ -463,12 +516,13 @@ class QmtStrategyManager:
|
|
|
463
516
|
result.update(error=self.load_error, message=self.load_error)
|
|
464
517
|
return result
|
|
465
518
|
|
|
466
|
-
def configure(self, row, identities, force=False):
|
|
519
|
+
def configure(self, row, identities, force=False, scripts_dir=None):
|
|
467
520
|
if self.load_error:
|
|
468
521
|
raise ValueError(self.load_error)
|
|
469
522
|
settings = normalize_strategy_settings(row.get("qmt_strategy"))
|
|
470
523
|
mode = normalize_strategy_mode(row.get("mode"))
|
|
471
|
-
enabled = row.get("enabled", True) and settings["enabled"]
|
|
524
|
+
enabled = row.get("enabled", True) and settings["enabled"]
|
|
525
|
+
scripts_dir = Path(scripts_dir) if scripts_dir is not None else self.scripts_dir
|
|
472
526
|
with self.lock:
|
|
473
527
|
wanted = set()
|
|
474
528
|
grouped = {}
|
|
@@ -486,19 +540,16 @@ class QmtStrategyManager:
|
|
|
486
540
|
"account_key": row["account_key"]}])
|
|
487
541
|
market = info.get("market") or identity.get("market") or ""
|
|
488
542
|
role = market or info.get("qmt_role") or "normal"
|
|
489
|
-
filename =
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
source_path = self.scripts_dir / "同账号独立市场" / (Path(filename).stem + "_" + market + ".py")
|
|
495
|
-
if not source_path.is_file():
|
|
496
|
-
raise ValueError("QMT 内置脚本不存在: %s" % source_path)
|
|
543
|
+
filename = entry_script_for_mode(mode, role, market)
|
|
544
|
+
source_path = scripts_dir / filename
|
|
545
|
+
if not source_path.is_file():
|
|
546
|
+
raise ValueError("QMT 内置脚本不存在: %s" % source_path)
|
|
547
|
+
read_entry_source(source_path)
|
|
497
548
|
for key in ("updated_at", "updated_at_text"):
|
|
498
549
|
identity.pop(key, None)
|
|
499
550
|
group_key = _digest([os.path.normcase(str(root)), row["account_id"]])[:24]
|
|
500
551
|
grouped.setdefault(group_key, {"root": str(root), "roles": []})["roles"].append({
|
|
501
|
-
"role": role, "source": str(source_path), "identity": identity,
|
|
552
|
+
"role": role, "source": str(source_path), "entry_script": filename, "identity": identity,
|
|
502
553
|
"source_sha256": hashlib.sha256(source_path.read_bytes()).hexdigest(),
|
|
503
554
|
"account_key": settings["account_keys"].get(role, "")})
|
|
504
555
|
if not grouped:
|
|
@@ -372,7 +372,7 @@ def test_auto_deploy_local_core_skips_when_target_matches(tmp_path):
|
|
|
372
372
|
def test_local_entry_sync_overwrites_bound_qmt_python_script(tmp_path):
|
|
373
373
|
source = tmp_path / "source"
|
|
374
374
|
(source / "qmt_scripts").mkdir(parents=True)
|
|
375
|
-
(source / "qmt_scripts" / "CFQUANT_LITE.py").write_bytes(b"
|
|
375
|
+
(source / "qmt_scripts" / "CFQUANT_LITE.py").write_bytes(b'LITE_ENTRY_VERSION = "test"\n')
|
|
376
376
|
qmt_root = tmp_path / "QMT"
|
|
377
377
|
(qmt_root / "bin.x64").mkdir(parents=True)
|
|
378
378
|
(qmt_root / "python").mkdir()
|
|
@@ -389,7 +389,7 @@ def test_local_entry_sync_overwrites_bound_qmt_python_script(tmp_path):
|
|
|
389
389
|
)
|
|
390
390
|
|
|
391
391
|
assert result["updated"] is True
|
|
392
|
-
assert entry.read_bytes() == b"
|
|
392
|
+
assert entry.read_bytes() == b'LITE_ENTRY_VERSION = "test"\n'
|
|
393
393
|
assert result["entry_file"] == str(entry)
|
|
394
394
|
assert result["backup"]
|
|
395
395
|
assert (qmt_root / "bin.x64" / ".cfquant_updates" / "entry_backups").is_dir()
|
|
@@ -402,7 +402,7 @@ def test_auto_deploy_project_update_copies_bound_mode_entry(tmp_path, monkeypatc
|
|
|
402
402
|
for name in ("__init__.py", "client.py", "protocol.py"):
|
|
403
403
|
(source_core / name).write_text("# source\n", encoding="utf-8")
|
|
404
404
|
(source / "qmt_scripts").mkdir()
|
|
405
|
-
(source / "qmt_scripts" / "CFQUANT_LITE.py").write_bytes(b"latest
|
|
405
|
+
(source / "qmt_scripts" / "CFQUANT_LITE.py").write_bytes(b'LITE_ENTRY_VERSION = "latest"\n')
|
|
406
406
|
|
|
407
407
|
qmt_root = tmp_path / "QMT"
|
|
408
408
|
target_core = qmt_root / "bin.x64" / "cfquant"
|
|
@@ -423,7 +423,7 @@ def test_auto_deploy_project_update_copies_bound_mode_entry(tmp_path, monkeypatc
|
|
|
423
423
|
|
|
424
424
|
assert result["summary"]["ok"] is True
|
|
425
425
|
assert result["summary"]["entry_updated_count"] == 1
|
|
426
|
-
assert (qmt_root / "python" / "CFQUANT_LITE.py").read_bytes() == b"latest
|
|
426
|
+
assert (qmt_root / "python" / "CFQUANT_LITE.py").read_bytes() == b'LITE_ENTRY_VERSION = "latest"\n'
|
|
427
427
|
assert result["results"][0]["entry_script"] == "CFQUANT_LITE.py"
|
|
428
428
|
|
|
429
429
|
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Verify the bytes copied and imported for Lite across binding/update paths."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import shutil
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from cfquant.qmt_strategy_deploy import QmtStrategyManager
|
|
11
|
+
from cfquant.qmt_strategy_package import _cipher
|
|
12
|
+
from cfquant.tests.test_qmt_strategy_deploy import deployment, finish_import, models, SCRIPTS
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def embedded_source(package):
|
|
16
|
+
content = json.loads(_cipher().decrypt(package.read_bytes()).decode("utf-8", "surrogateescape"))["content"]
|
|
17
|
+
return next(node.args[0].value for node in ast.walk(ast.parse(content))
|
|
18
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name)
|
|
19
|
+
and node.func.id == "compile" and isinstance(node.args[0], ast.Constant))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def assert_lite_package(manager, root):
|
|
23
|
+
job = next(job for job in manager.jobs.values() if job.get("enabled"))
|
|
24
|
+
assert job["mode"] == "lite"
|
|
25
|
+
for role in job["roles"]:
|
|
26
|
+
source = embedded_source(root / "formulas" / (role["name"] + ".rzrk"))
|
|
27
|
+
assert "LITE_ENTRY_VERSION =" in source
|
|
28
|
+
assert "def account_route_client_ids(" in source
|
|
29
|
+
assert not any(isinstance(node, ast.ImportFrom) and (node.module or "").startswith("cfquant")
|
|
30
|
+
for node in ast.walk(ast.parse(source)))
|
|
31
|
+
assert Path(role["source"]).name.startswith("CFQUANT_LITE")
|
|
32
|
+
return job
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.fixture
|
|
36
|
+
def web_deployment(deployment, tmp_path, monkeypatch):
|
|
37
|
+
import cfquant_web_server as web
|
|
38
|
+
manager, row, infos, running, root = deployment
|
|
39
|
+
config = web.WebRuntimeConfig(str(tmp_path / "web.json"), str(tmp_path / "web.db"))
|
|
40
|
+
config._data["account_configs"] = {row["account_key"]: row}
|
|
41
|
+
monkeypatch.setattr(web, "WEB_CONFIG", config)
|
|
42
|
+
monkeypatch.setattr(web, "BASE_DIR", str(SCRIPTS.parent))
|
|
43
|
+
monkeypatch.setattr(web, "QMT_STRATEGIES", manager)
|
|
44
|
+
updater = web.CfquantUpdater(config)
|
|
45
|
+
monkeypatch.setattr(updater, "install_local_core_to_qmt_dir", lambda *args, **kwargs: {"skipped": True})
|
|
46
|
+
monkeypatch.setattr(web, "UPDATER", updater)
|
|
47
|
+
monkeypatch.setattr(web, "write_qmt_bridge_identity", lambda row: dict(infos[0]))
|
|
48
|
+
monkeypatch.setattr(web, "write_qmt_market_bridge_identities", lambda row: [])
|
|
49
|
+
monkeypatch.setattr(web, "sync_qmt_bridge_identities", lambda: [])
|
|
50
|
+
monkeypatch.setattr(web, "qmt_process_preflight", lambda **kwargs: None)
|
|
51
|
+
monkeypatch.setattr(web, "qmt_auto_login_apply_for_account", lambda *args, **kwargs: {})
|
|
52
|
+
monkeypatch.setattr(web, "ensure_account_runtime", lambda mode: {})
|
|
53
|
+
monkeypatch.setattr(web.ACCOUNT_CACHE, "prime_configured_accounts", lambda: None)
|
|
54
|
+
monkeypatch.setattr(web.STATUS_MONITOR, "wake", lambda: None)
|
|
55
|
+
monkeypatch.setattr(web.CALLBACKS, "refresh_channels", lambda channels: None)
|
|
56
|
+
return web, manager, row, infos, root
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pytest.mark.parametrize("action", ["bind", "account_update", "project_update", "startup"])
|
|
60
|
+
def test_selected_lite_replaces_imported_generic_code(action, web_deployment, monkeypatch):
|
|
61
|
+
web, manager, row, infos, root = web_deployment
|
|
62
|
+
manager.configure(row, infos)
|
|
63
|
+
finish_import(manager, root)
|
|
64
|
+
manager.process_once()
|
|
65
|
+
generic_name = next(iter(manager.jobs.values()))["roles"][0]["name"]
|
|
66
|
+
# Simulate an older installation with a Lite binding and a generic import.
|
|
67
|
+
row["mode"] = "lite"
|
|
68
|
+
if action == "bind":
|
|
69
|
+
result = web.save_account_runtime_config({
|
|
70
|
+
key: row[key] for key in ("account_id", "account_type", "bridge_id", "qmt_dir", "mode")})
|
|
71
|
+
assert result["account"]["mode"] == "lite"
|
|
72
|
+
assert not result["qmt_strategy_deploy"].get("error")
|
|
73
|
+
elif action == "account_update":
|
|
74
|
+
monkeypatch.setattr(web, "account_config_for_request", lambda body: row)
|
|
75
|
+
result = web.update_account_qmt_core({"account_key": row["account_key"]})
|
|
76
|
+
assert not result["qmt_strategy_deploy"].get("error")
|
|
77
|
+
elif action == "project_update":
|
|
78
|
+
result = web.auto_deploy_qmt_core_for_all_accounts(str(SCRIPTS.parent), sync_entry_scripts=True)
|
|
79
|
+
assert result["summary"]["ok"]
|
|
80
|
+
else:
|
|
81
|
+
# Persisted jobs must be repaired on first start after an old updater.
|
|
82
|
+
manager = QmtStrategyManager(manager.path, SCRIPTS, lambda root: False)
|
|
83
|
+
monkeypatch.setattr(web, "QMT_STRATEGIES", manager)
|
|
84
|
+
result = web.refresh_bound_qmt_strategies()
|
|
85
|
+
assert not result[0].get("error")
|
|
86
|
+
assert_lite_package(manager, root)
|
|
87
|
+
assert models(root)[generic_name].getAttribute("startupAutorun") == "0"
|
|
88
|
+
if action != "startup":
|
|
89
|
+
assert (root / "python" / "CFQUANT_LITE.py").read_bytes() == (SCRIPTS / "CFQUANT_LITE.py").read_bytes()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_edit_without_mode_preserves_lite_binding_and_payload(web_deployment):
|
|
93
|
+
web, manager, row, _, root = web_deployment
|
|
94
|
+
row["mode"] = "lite"
|
|
95
|
+
result = web.save_account_runtime_config({
|
|
96
|
+
key: row[key] for key in ("account_id", "account_type", "bridge_id", "qmt_dir")})
|
|
97
|
+
assert result["account"]["mode"] == "lite"
|
|
98
|
+
assert_lite_package(manager, root)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_edit_advanced_binding_to_lite_retires_both_advanced_models(web_deployment):
|
|
102
|
+
web, manager, row, infos, root = web_deployment
|
|
103
|
+
trade_root = root.with_name("QMT_trade")
|
|
104
|
+
shutil.copytree(root, trade_root)
|
|
105
|
+
row.update(mode="lttx", qmt_trade_dir=str(trade_root / "bin.x64"))
|
|
106
|
+
row["qmt_strategy"]["autorun"] = True
|
|
107
|
+
trade_info = dict(infos[0], qmt_role="trade", core_dir=str(trade_root / "bin.x64"),
|
|
108
|
+
path=str(trade_root / "bin.x64" / "cfquant_bridge_config.json"))
|
|
109
|
+
manager.configure(row, infos + [trade_info])
|
|
110
|
+
advanced_name = "CFQ_%s_LTTX" % row["account_id"]
|
|
111
|
+
assert models(root)[advanced_name].getAttribute("startupAutorun") == "1"
|
|
112
|
+
assert models(trade_root)[advanced_name].getAttribute("startupAutorun") == "1"
|
|
113
|
+
|
|
114
|
+
body = {key: row[key] for key in ("account_id", "account_type", "bridge_id", "qmt_dir")}
|
|
115
|
+
result = web.save_account_runtime_config(dict(body, mode="lite", qmt_trade_dir=""))
|
|
116
|
+
|
|
117
|
+
assert result["account"]["mode"] == "lite"
|
|
118
|
+
assert result["account"]["qmt_trade_dir"] == ""
|
|
119
|
+
assert not result["qmt_strategy_deploy"].get("error")
|
|
120
|
+
job = assert_lite_package(manager, root)
|
|
121
|
+
assert job["roles"][0]["name"] == "CFQ_%s_LITE" % row["account_id"]
|
|
122
|
+
assert models(root)[job["roles"][0]["name"]].getAttribute("startupAutorun") == "1"
|
|
123
|
+
assert models(root)[advanced_name].getAttribute("startupAutorun") == "0"
|
|
124
|
+
assert models(trade_root)[advanced_name].getAttribute("startupAutorun") == "0"
|
|
125
|
+
trade_job = next(job for job in manager.jobs.values() if job["root"] == str(trade_root))
|
|
126
|
+
assert trade_job["enabled"] is False
|
|
127
|
+
assert trade_job["state"] == "disabled"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@pytest.mark.parametrize("single_account", [False, True])
|
|
131
|
+
def test_shared_qmt_directory_does_not_drop_lite_market_entries(single_account, web_deployment):
|
|
132
|
+
web, _, row, _, root = web_deployment
|
|
133
|
+
row.update(mode="lite", market_routing_enabled=True,
|
|
134
|
+
qmt_strategy={"enabled": False}, market_bridges={
|
|
135
|
+
market: {"qmt_dir": str(root), "bridge_id": "bridge_" + market}
|
|
136
|
+
for market in ("SH", "SZ")})
|
|
137
|
+
if single_account:
|
|
138
|
+
result = web.auto_deploy_qmt_core_for_account(row)
|
|
139
|
+
else:
|
|
140
|
+
# The earlier binding must not win solely because it shares a directory.
|
|
141
|
+
generic = dict(row, account_key="generic", account_id="222", mode="ctypes", market_routing_enabled=False)
|
|
142
|
+
web.WEB_CONFIG._data["account_configs"] = {"generic": generic, row["account_key"]: row}
|
|
143
|
+
result = web.auto_deploy_qmt_core_for_all_accounts(str(SCRIPTS.parent), sync_entry_scripts=True)
|
|
144
|
+
assert result["summary"]["ok"]
|
|
145
|
+
for name, directory in (("CFQUANT_LITE.py", SCRIPTS),
|
|
146
|
+
("CFQUANT_LITE_SH.py", SCRIPTS / "同账号独立市场"),
|
|
147
|
+
("CFQUANT_LITE_SZ.py", SCRIPTS / "同账号独立市场")):
|
|
148
|
+
assert (root / "python" / name).read_bytes() == (directory / name).read_bytes()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_update_uses_new_source_and_rejects_mislabeled_generic_entry(web_deployment, tmp_path):
|
|
152
|
+
web, manager, row, _, root = web_deployment
|
|
153
|
+
row["mode"] = "lite"
|
|
154
|
+
source = tmp_path / "updated_project"
|
|
155
|
+
(source / "qmt_scripts").mkdir(parents=True)
|
|
156
|
+
entry = source / "qmt_scripts" / "CFQUANT_LITE.py"
|
|
157
|
+
entry.write_bytes((SCRIPTS / entry.name).read_bytes() + b'\nRELEASE_TEST_MARKER = "new-source"\n')
|
|
158
|
+
result = web.auto_deploy_qmt_core_for_all_accounts(str(source), sync_entry_scripts=True)
|
|
159
|
+
assert result["summary"]["ok"]
|
|
160
|
+
job = assert_lite_package(manager, root)
|
|
161
|
+
assert 'RELEASE_TEST_MARKER = "new-source"' in embedded_source(
|
|
162
|
+
root / "formulas" / (job["roles"][0]["name"] + ".rzrk"))
|
|
163
|
+
previous = (root / "python" / entry.name).read_bytes()
|
|
164
|
+
shutil.copyfile(SCRIPTS / "CFQUANT_CTYPE_ALL_LOWLAT.py", entry)
|
|
165
|
+
result = web.auto_deploy_qmt_core_for_all_accounts(str(source), sync_entry_scripts=True)
|
|
166
|
+
assert not result["summary"]["ok"]
|
|
167
|
+
assert result["summary"]["strategy_error_count"] == 1
|
|
168
|
+
assert "通用模式代码" in result["summary"]["message"]
|
|
169
|
+
assert (root / "python" / entry.name).read_bytes() == previous
|
|
@@ -15,6 +15,7 @@ from cfquant.qmt_strategy_deploy import (
|
|
|
15
15
|
QmtStrategyManager, _account_binding, _atomic_write, _read_document,
|
|
16
16
|
_write_json, _resolve_account_binding, account_qmt_roots, managed_source, normalize_strategy_settings,
|
|
17
17
|
normalize_strategy_mode,
|
|
18
|
+
entry_script_for_mode, read_entry_source,
|
|
18
19
|
_strategy_slot_name,
|
|
19
20
|
)
|
|
20
21
|
from cfquant.qmt_strategy_package import _cipher, build_package
|
|
@@ -31,6 +32,14 @@ def test_strategy_mode_aliases_are_canonical(value, expected):
|
|
|
31
32
|
assert normalize_strategy_mode(value) == expected
|
|
32
33
|
|
|
33
34
|
|
|
35
|
+
def test_lite_entry_mapping_rejects_generic_source(tmp_path):
|
|
36
|
+
assert entry_script_for_mode("extreme") == "CFQUANT_LITE.py"
|
|
37
|
+
source = tmp_path / "CFQUANT_LITE.py"
|
|
38
|
+
source.write_text("from cfquant import __version__\n", encoding="utf-8")
|
|
39
|
+
with pytest.raises(ValueError, match="通用模式代码"):
|
|
40
|
+
read_entry_source(source)
|
|
41
|
+
|
|
42
|
+
|
|
34
43
|
def test_strategy_name_contains_mode_and_shortens_long_account():
|
|
35
44
|
name = _strategy_slot_name("ACCOUNT-12345678901234567890", "lite")
|
|
36
45
|
assert name == "CFQ_67890_LITE"
|
|
@@ -455,6 +464,8 @@ def test_all_managed_entry_variants_compile_without_executing(mode, deployment):
|
|
|
455
464
|
assert document["detail"]["realName"] == role["name"] + ".py"
|
|
456
465
|
assert document["formulaCatalog"][0].encode("utf-8", "surrogateescape").hex() == "ced2b5c4b2dfc2d4"
|
|
457
466
|
ast.parse(document["content"], feature_version=(3, 6))
|
|
467
|
+
assert "# Mode: %r\n" % mode in document["content"]
|
|
468
|
+
assert "# Entry: %r\n" % Path(role["source"]).name in document["content"]
|
|
458
469
|
if mode == "lite":
|
|
459
470
|
embedded = [node.args[0].value for node in ast.walk(ast.parse(document["content"]))
|
|
460
471
|
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name)
|
|
@@ -468,6 +479,8 @@ def test_all_managed_entry_variants_compile_without_executing(mode, deployment):
|
|
|
468
479
|
if isinstance(node, (ast.FunctionDef, ast.ClassDef))}
|
|
469
480
|
assert definitions(actual) == definitions(original)
|
|
470
481
|
assert len(embedded[0].splitlines()) > 7000
|
|
482
|
+
assert len(document["content"].splitlines()) > 7000
|
|
483
|
+
assert max(map(len, document["content"].splitlines())) < 2000
|
|
471
484
|
descriptor = {"mode": mode, "generation": "test", "role": "SH", "control_path": "test", "lease_dir": "test"}
|
|
472
485
|
variants = list((SCRIPTS / "同账号独立市场").glob("*.py")) + [SCRIPTS / "CFQUANT_TRADE_LOWLAT.py"]
|
|
473
486
|
for path in variants:
|
|
@@ -513,6 +526,64 @@ def test_process_once_detects_stale_runtime_generation_and_queues_reimport(deplo
|
|
|
513
526
|
assert (root / "formulas" / (role["name"] + ".rzrk")).is_file()
|
|
514
527
|
|
|
515
528
|
|
|
529
|
+
@pytest.mark.parametrize("report_state", ["running", "stopped"])
|
|
530
|
+
@pytest.mark.parametrize("previous_mode", ["lite", "lttx"])
|
|
531
|
+
def test_old_runtime_report_does_not_reset_prepared_replacement(deployment, report_state, previous_mode):
|
|
532
|
+
manager, row, infos, running, root = deployment
|
|
533
|
+
row["mode"] = "lite"
|
|
534
|
+
manager.configure(row, infos)
|
|
535
|
+
finish_import(manager, root)
|
|
536
|
+
manager.process_once()
|
|
537
|
+
first = copy.deepcopy(next(iter(manager.jobs.values())))
|
|
538
|
+
_write_json(first["roles"][0]["runtime_status_path"], {
|
|
539
|
+
"state": report_state, "generation": first["generation"], "updated_at": time.time() - 120,
|
|
540
|
+
})
|
|
541
|
+
if previous_mode != "lite":
|
|
542
|
+
row["mode"] = previous_mode
|
|
543
|
+
manager.configure(row, infos)
|
|
544
|
+
finish_import(manager, root)
|
|
545
|
+
manager.process_once()
|
|
546
|
+
|
|
547
|
+
row["mode"] = "lite"
|
|
548
|
+
manager.configure(row, infos, force=True)
|
|
549
|
+
job = next(iter(manager.jobs.values()))
|
|
550
|
+
role = job["roles"][0]
|
|
551
|
+
queue = root / "formulas" / (role["name"] + ".rzrk")
|
|
552
|
+
assert queue.is_file()
|
|
553
|
+
assert role["model_prepared"] is True
|
|
554
|
+
|
|
555
|
+
# The old heartbeat remains on disk until QMT has imported and started
|
|
556
|
+
# the new strategy. Repeated polling or a Web restart must not undo it.
|
|
557
|
+
manager = QmtStrategyManager(manager.path, SCRIPTS, lambda root: running[0])
|
|
558
|
+
job = next(iter(manager.jobs.values()))
|
|
559
|
+
role = job["roles"][0]
|
|
560
|
+
running[0] = True
|
|
561
|
+
manager.process_once()
|
|
562
|
+
manager.process_once()
|
|
563
|
+
assert role["reimport_required"] is False
|
|
564
|
+
assert role["model_prepared"] is True
|
|
565
|
+
assert role["queued"] is True
|
|
566
|
+
assert job["state"] == "waiting_manual_start"
|
|
567
|
+
|
|
568
|
+
finish_import(manager, root)
|
|
569
|
+
container = root / "python" / (role["name"] + ".py")
|
|
570
|
+
running[0] = False
|
|
571
|
+
manager.process_once()
|
|
572
|
+
manager.process_once()
|
|
573
|
+
assert container.is_file()
|
|
574
|
+
assert not queue.exists()
|
|
575
|
+
assert role["imported"] is True
|
|
576
|
+
assert job["state"] == "configured"
|
|
577
|
+
|
|
578
|
+
_write_json(role["runtime_status_path"], {
|
|
579
|
+
"state": "running", "generation": job["generation"], "updated_at": time.time(),
|
|
580
|
+
})
|
|
581
|
+
running[0] = True
|
|
582
|
+
manager.process_once()
|
|
583
|
+
assert job["state"] == "running"
|
|
584
|
+
assert not job.get("control_generation")
|
|
585
|
+
|
|
586
|
+
|
|
516
587
|
@pytest.mark.parametrize("encoding", ["utf-8", "gbk", "gb18030"])
|
|
517
588
|
def test_managed_package_executes_after_qmt_encoding_with_chinese_paths(tmp_path, encoding):
|
|
518
589
|
directory = tmp_path / "\u534e\u6cf0\u8bc1\u5238 QMT \u6a21\u62df"
|
|
@@ -282,8 +282,9 @@ def test_save_and_delete_call_deployment_manager(web_config, monkeypatch):
|
|
|
282
282
|
class Manager:
|
|
283
283
|
def reconcile(self, accounts):
|
|
284
284
|
calls.append(("reconcile", set(accounts)))
|
|
285
|
-
def configure(self, row, identities, force=False):
|
|
285
|
+
def configure(self, row, identities, force=False, scripts_dir=None):
|
|
286
286
|
assert force is True
|
|
287
|
+
assert scripts_dir == os.path.join(web.BASE_DIR, "qmt_scripts")
|
|
287
288
|
calls.append(("configure", row["qmt_strategy"], identities))
|
|
288
289
|
return {"enabled": True, "targets": [{"state": "waiting_exit"}]}
|
|
289
290
|
monkeypatch.setattr(web, "QMT_STRATEGIES", Manager())
|
|
@@ -303,7 +304,7 @@ def test_save_and_delete_call_deployment_manager(web_config, monkeypatch):
|
|
|
303
304
|
assert calls[-1] == ("reconcile", set())
|
|
304
305
|
|
|
305
306
|
|
|
306
|
-
def test_save_can_start_qmt_auto_login_and_persist_restart_times(web_config, monkeypatch):
|
|
307
|
+
def test_save_can_start_qmt_auto_login_and_persist_restart_times(web_config, monkeypatch, tmp_path):
|
|
307
308
|
web, config = web_config
|
|
308
309
|
calls = []
|
|
309
310
|
monkeypatch.setattr(web, "auto_deploy_qmt_core_for_account", lambda *args, **kwargs: {})
|
|
@@ -324,7 +325,7 @@ def test_save_can_start_qmt_auto_login_and_persist_restart_times(web_config, mon
|
|
|
324
325
|
data = web.save_account_runtime_config({
|
|
325
326
|
"account_id": "8885060548",
|
|
326
327
|
"display_name": "国金证券",
|
|
327
|
-
"qmt_dir":
|
|
328
|
+
"qmt_dir": str(tmp_path / "QMT" / "bin.x64"),
|
|
328
329
|
"qmt_auto_login": {"enabled": True, "restart_times": ["6:30", "06:30", "18:05"]},
|
|
329
330
|
})
|
|
330
331
|
assert data["qmt_auto_login"]["pid"] == 1234
|
|
@@ -99,6 +99,7 @@ cfquant/tests/test_cftrader_web.py
|
|
|
99
99
|
cfquant/tests/test_credit_compat.py
|
|
100
100
|
cfquant/tests/test_json_serialization.py
|
|
101
101
|
cfquant/tests/test_lite_account_routing.py
|
|
102
|
+
cfquant/tests/test_lite_deployment_paths.py
|
|
102
103
|
cfquant/tests/test_local_trading_calendar.py
|
|
103
104
|
cfquant/tests/test_log_management.py
|
|
104
105
|
cfquant/tests/test_management.py
|