cfquant 0.2.61__tar.gz → 0.2.62__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.61 → cfquant-0.2.62}/PKG-INFO +1 -1
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/qmt_component_login.py +16 -7
- cfquant-0.2.62/cfquant/qmt_strategy_agent.js +239 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/qmt_strategy_recovery.py +24 -25
- cfquant-0.2.62/cfquant/tests/test_qmt_strategy_capabilities.py +189 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_qmt_strategy_recovery.py +59 -4
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/version.py +2 -2
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant.egg-info/PKG-INFO +1 -1
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant.egg-info/SOURCES.txt +1 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/pyproject.toml +1 -1
- {cfquant-0.2.61 → cfquant-0.2.62}/qmt_scripts/CFQUANT_LITE.py +1 -1
- {cfquant-0.2.61 → cfquant-0.2.62}/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.61 → cfquant-0.2.62}/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.61 → cfquant-0.2.62}/web_dashboard/app.js +1 -1
- {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/index.html +2 -2
- cfquant-0.2.61/cfquant/qmt_strategy_agent.js +0 -131
- {cfquant-0.2.61 → cfquant-0.2.62}/LICENSE +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/LTtx/__init__.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/LTtx/tx/Config.txt +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/LTtx/tx/LTtx_server.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/LTtx/tx/__init__.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/LTtx/tx/tx.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/MANIFEST.in +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/README.md +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/__init__.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/_editable_install.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/account_routing.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/algo.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/algo_broker.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/batch_orders.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/build_info.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/callback_dispatcher.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/cftrader.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/channels.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/cli.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/client.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/config.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/diagnostics.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/flow_control.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/hub.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/level2.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/log_management.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/logging_i18n.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/management.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/normal_bridge.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/order_identity.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/order_meta.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/pipe_bridge.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/pipe_client.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/pipe_hub.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/pipe_transport.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/protocol.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/qmt_binding_defaults.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/qmt_binding_settings.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/qmt_bridge.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/qmt_component_agent.js +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/qmt_credentials.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/qmt_dependencies.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/qmt_native_worker.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/qmt_pipe_runtime.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/qmt_strategy_deploy.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/qmt_strategy_package.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/qmt_strategy_runtime.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/qmt_update.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/quote_history.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/quote_leases.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/quote_service.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/remote_adapter.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/remote_management.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/remote_supervisor.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/rpc_timing.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/runtime_report.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/shared_quotes.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/stock_connect.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/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.61 → cfquant-0.2.62}/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.61 → cfquant-0.2.62}/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.61 → cfquant-0.2.62}/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.61 → cfquant-0.2.62}/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.61 → cfquant-0.2.62}/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.61 → cfquant-0.2.62}/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.61 → cfquant-0.2.62}/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.61 → cfquant-0.2.62}/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.61 → cfquant-0.2.62}/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.61 → cfquant-0.2.62}/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.61 → cfquant-0.2.62}/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.61 → cfquant-0.2.62}/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.61 → cfquant-0.2.62}/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.61 → cfquant-0.2.62}/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.61 → cfquant-0.2.62}/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.61 → cfquant-0.2.62}/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.61 → cfquant-0.2.62}/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.61 → cfquant-0.2.62}/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.61 → cfquant-0.2.62}/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.61 → cfquant-0.2.62}/cfquant/tests/29_Level2/350/241/214/346/203/205/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/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.61 → cfquant-0.2.62}/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.61 → cfquant-0.2.62}/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.61 → cfquant-0.2.62}/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.61 → cfquant-0.2.62}/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.61 → cfquant-0.2.62}/cfquant/tests/34_/345/205/250/346/216/250/345/206/205/351/203/250/345/273/266/350/277/237/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/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.61 → cfquant-0.2.62}/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.61 → cfquant-0.2.62}/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.61 → cfquant-0.2.62}/cfquant/tests/6_/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/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.61 → cfquant-0.2.62}/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.61 → cfquant-0.2.62}/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.61 → cfquant-0.2.62}/cfquant/tests/_helpers.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/readme.md +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_account_query_timing.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_account_subscription_probe.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_advanced_parity.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_algo.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_algo_broker.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_api_tester_browser.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_api_tester_web.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_binding_live_refresh_browser.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_binding_runtime_browser.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_callback_recovery.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_callback_requests.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_cftrader.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_cftrader_tutorial.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_cftrader_web.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_credit_compat.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_dataframe_encoding.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_json_serialization.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_latency_flow_control.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_lite_account_routing.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_lite_deployment_paths.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_lite_parity.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_lite_qmt_dispatch.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_local_trading_calendar.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_log_management.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_management.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_management_batch.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_market_data_fallback.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_market_timing.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_order_identity.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_order_time_callback.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_pipe_shutdown.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_project_updater.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_python_api_reference.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_qmt_auto_login_browser.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_qmt_auto_login_integration.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_qmt_binding_defaults.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_qmt_binding_settings.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_qmt_component_login.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_qmt_dependencies.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_qmt_fileless.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_qmt_native_worker.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_qmt_strategy_browser.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_qmt_strategy_deploy.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_qmt_strategy_web.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_qmt_update.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_quote_latency.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_quote_latency_encoding.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_quote_service.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_remote_management.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_remote_management_browser.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_remote_version_check.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_shared_quotes.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_shared_quotes_generated.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_startup_recovery.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_stock_connect.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_stock_connect_browser.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_subscription_congestion.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_table_conversion.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_trade_strategy_callback.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_trader_subscription_recovery.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_trading_dates.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_trading_monitor.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_trading_monitor_browser.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_tutorial_reader.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_tx_auto_import.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_tx_shutdown.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/trading_calendar.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/trading_monitor.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tx.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tx_trade_bridge.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/xtconstant.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/xtdata.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/xttrader.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/xttype.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant.egg-info/dependency_links.txt +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant.egg-info/entry_points.txt +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant.egg-info/requires.txt +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant.egg-info/top_level.txt +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant_pipe_hub.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/cfquant_web_server.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/qmt_scripts/CFQUANT.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/qmt_scripts/__init__.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/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.61 → cfquant-0.2.62}/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.61 → cfquant-0.2.62}/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.61 → cfquant-0.2.62}/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.61 → cfquant-0.2.62}/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.61 → cfquant-0.2.62}/requirements.txt +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/setup.cfg +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/__init__.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/api-tester.js +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/avatars/amber-pulse.svg +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/avatars/copper-grid.svg +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/avatars/market-blue.svg +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/avatars/rose-circuit.svg +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/avatars/signal-green.svg +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/avatars/slate-wave.svg +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/avatars/teal-orbit.svg +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/avatars/violet-node.svg +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/cftrader-api-data.js +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/docs.js +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/favicon.svg +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/python-api-data.js +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/python-api.js +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/remote-management.js +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/styles.css +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/trading-monitor.js +0 -0
- {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/vendor/mermaid.min.js +0 -0
|
@@ -27,7 +27,7 @@ def verify_environment(directory):
|
|
|
27
27
|
verify_environment_isolated(directory)
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
def _verify_environment_native(directory):
|
|
30
|
+
def _verify_environment_native(directory, *, strategy=False):
|
|
31
31
|
"""Only invoked inside the native worker or the standalone login CLI."""
|
|
32
32
|
try:
|
|
33
33
|
from .qmt_dependencies import LOGIN_MODULES, import_login_module
|
|
@@ -38,14 +38,17 @@ def _verify_environment_native(directory):
|
|
|
38
38
|
directory = Path(directory)
|
|
39
39
|
for module in ("Qt5Core.dll", "Qt5Widgets.dll"):
|
|
40
40
|
version = win32api.GetFileVersionInfo(str(directory / module), "\\")
|
|
41
|
-
if
|
|
41
|
+
if strategy:
|
|
42
|
+
if version["FileVersionMS"] >> 16 != 5:
|
|
43
|
+
raise RuntimeError("QMT 策略检测需要兼容的 Qt 5 接口,当前组件 ABI 不受支持")
|
|
44
|
+
elif (version["FileVersionMS"], version["FileVersionLS"]) != ((5 << 16) | 6, 2 << 16):
|
|
42
45
|
raise RuntimeError("QMT 组件自动登录当前仅支持 Qt 5.6.2,请先适配此客户端版本")
|
|
43
46
|
with (directory / "XtItClient.exe").open("rb") as stream:
|
|
44
47
|
stream.seek(0x3C)
|
|
45
48
|
offset = int.from_bytes(stream.read(4), "little")
|
|
46
49
|
stream.seek(offset + 4)
|
|
47
50
|
if int.from_bytes(stream.read(2), "little") != 0x8664:
|
|
48
|
-
raise RuntimeError("QMT
|
|
51
|
+
raise RuntimeError("QMT 原生组件操作需要 64 位客户端")
|
|
49
52
|
|
|
50
53
|
|
|
51
54
|
def auto_login(backend, password_provider, *, retry_not_ready=False, login_account=None,
|
|
@@ -132,7 +135,7 @@ def select_binding(config_path, name):
|
|
|
132
135
|
|
|
133
136
|
|
|
134
137
|
class QtLoginBackend:
|
|
135
|
-
def __init__(self, directory, timeout=40, expected_pid=None, *, agent_extension=""):
|
|
138
|
+
def __init__(self, directory, timeout=40, expected_pid=None, *, agent_extension="", strategy=False):
|
|
136
139
|
try:
|
|
137
140
|
from .qmt_dependencies import import_login_module
|
|
138
141
|
except ImportError:
|
|
@@ -170,7 +173,10 @@ class QtLoginBackend:
|
|
|
170
173
|
self.pid = matches[0].pid
|
|
171
174
|
if expected_pid is not None and self.pid != expected_pid:
|
|
172
175
|
raise RuntimeError("QMT process changed; stopping this login attempt")
|
|
173
|
-
|
|
176
|
+
if strategy:
|
|
177
|
+
_verify_environment_native(directory, strategy=True)
|
|
178
|
+
else:
|
|
179
|
+
_verify_environment_native(directory)
|
|
174
180
|
self.mutex = win32event.CreateMutex(None, False, "Local\\CFQuantQmtComponentLogin_" + str(self.pid))
|
|
175
181
|
acquired = win32event.WaitForSingleObject(self.mutex, 0)
|
|
176
182
|
if acquired not in (0, 0x80):
|
|
@@ -179,13 +185,16 @@ class QtLoginBackend:
|
|
|
179
185
|
raise RuntimeError("Another component login is already running for this QMT")
|
|
180
186
|
try:
|
|
181
187
|
self.session = frida.attach(self.pid)
|
|
182
|
-
|
|
188
|
+
modules = ["Qt5Core.dll", "Qt5Widgets.dll"]
|
|
189
|
+
if not strategy:
|
|
190
|
+
modules.append("LoginDialog.dll")
|
|
191
|
+
readiness = self.session.create_script("rpc.exports.ready = () => %s.every(n => Process.findModuleByName(n) !== null);" % json.dumps(modules))
|
|
183
192
|
readiness.load()
|
|
184
193
|
ready = readiness.exports_sync.ready()
|
|
185
194
|
readiness.unload()
|
|
186
195
|
if not ready:
|
|
187
196
|
raise QmtNotReady("QMT login modules are still loading")
|
|
188
|
-
source = Path(__file__).with_name("qmt_component_agent.js").read_text(encoding="utf-8")
|
|
197
|
+
source = "" if strategy else Path(__file__).with_name("qmt_component_agent.js").read_text(encoding="utf-8")
|
|
189
198
|
script = self.session.create_script(source + "\n" + agent_extension)
|
|
190
199
|
script.on("message", lambda message, data: self.messages.append("agent_error") if message.get("type") == "error" else None)
|
|
191
200
|
script.load()
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// A scope also permits an older Python host to append this agent to its login
|
|
4
|
+
// agent while the project is being upgraded.
|
|
5
|
+
(() => {
|
|
6
|
+
// Resolve query, start and notice APIs independently of the login agent.
|
|
7
|
+
const missing = {gui: [], query: [], start: [], notice: [], dismiss: []};
|
|
8
|
+
function native(group, moduleName, name, result, args) {
|
|
9
|
+
const module = Process.findModuleByName(moduleName);
|
|
10
|
+
const address = module && module.findExportByName(name);
|
|
11
|
+
if (!address) {
|
|
12
|
+
missing[group].push(moduleName + ':' + name);
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
return new NativeFunction(address, result, args);
|
|
16
|
+
}
|
|
17
|
+
const findWidget = native('gui', 'Qt5Widgets.dll', '?find@QWidget@@SAPEAV1@_K@Z', 'pointer', ['uint64']);
|
|
18
|
+
const inherits = native('gui', 'Qt5Core.dll', '?inherits@QObject@@QEBA_NPEBD@Z', 'bool', ['pointer', 'pointer']);
|
|
19
|
+
const getThread = native('gui', 'user32.dll', 'GetWindowThreadProcessId', 'uint32', ['pointer', 'pointer']);
|
|
20
|
+
const postMessage = native('gui', 'user32.dll', 'PostMessageW', 'bool', ['pointer', 'uint32', 'pointer', 'pointer']);
|
|
21
|
+
const dispatchModule = Process.findModuleByName('user32.dll');
|
|
22
|
+
const dispatchAddress = dispatchModule && dispatchModule.findExportByName('DispatchMessageW');
|
|
23
|
+
if (!dispatchAddress) missing.gui.push('user32.dll:DispatchMessageW');
|
|
24
|
+
if (Process.arch !== 'x64' || Process.pointerSize !== 8) missing.gui.push('x64 ABI');
|
|
25
|
+
const stringCtor = native('query', 'Qt5Core.dll', '??0QString@@QEAA@PEBVQChar@@H@Z', 'pointer', ['pointer', 'pointer', 'int']);
|
|
26
|
+
const stringDtor = native('query', 'Qt5Core.dll', '??1QString@@QEAA@XZ', 'void', ['pointer']);
|
|
27
|
+
const stringSize = native('query', 'Qt5Core.dll', '?size@QString@@QEBAHXZ', 'int', ['pointer']);
|
|
28
|
+
const stringData = native('query', 'Qt5Core.dll', '?utf16@QString@@QEBAPEBGXZ', 'pointer', ['pointer']);
|
|
29
|
+
const strategyManager = native('query', 'Formula.dll', '?instance@?$CSingletonT@VCStrategyTradeManager@TC@@@utils@@SAPEAVCStrategyTradeManager@TC@@XZ', 'pointer', []);
|
|
30
|
+
const strategySave = native('query', 'Formula.dll', '?saveSettings@CStrategyTradeManager@TC@@QEAAXAEAVQDomElement@@AEAVQDomDocument@@@Z', 'void', ['pointer', 'pointer', 'pointer']);
|
|
31
|
+
// isStrategyRun takes a request identifier; isFomrulaRun takes a formula name.
|
|
32
|
+
const strategyRunning = native('query', 'Formula.dll', '?isFomrulaRun@CStrategyTradeManager@TC@@QEAA_NAEBVQString@@@Z', 'bool', ['pointer', 'pointer']);
|
|
33
|
+
const strategyWhitelistReady = native('start', 'Formula.dll', '?isConnWhiteListReady@CStrategyTradeManager@TC@@QEBA_NXZ', 'bool', ['pointer']);
|
|
34
|
+
const strategyRun = native('start', 'Formula.dll', '?runStrategy@CStrategyTradeManager@TC@@QEAAXHAEBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z', 'void', ['pointer', 'int', 'pointer']);
|
|
35
|
+
const strategyDocCtor = native('query', 'Qt5Xml.dll', '??0QDomDocument@@QEAA@XZ', 'pointer', ['pointer']);
|
|
36
|
+
const strategyDocDtor = native('query', 'Qt5Xml.dll', '??1QDomDocument@@QEAA@XZ', 'void', ['pointer']);
|
|
37
|
+
const strategyElemDtor = native('query', 'Qt5Xml.dll', '??1QDomElement@@QEAA@XZ', 'void', ['pointer']);
|
|
38
|
+
const strategyCreateElem = native('query', 'Qt5Xml.dll', '?createElement@QDomDocument@@QEAA?AVQDomElement@@AEBVQString@@@Z', 'pointer', ['pointer', 'pointer', 'pointer']);
|
|
39
|
+
const strategyFirst = native('query', 'Qt5Xml.dll', '?firstChildElement@QDomNode@@QEBA?AVQDomElement@@AEBVQString@@@Z', 'pointer', ['pointer', 'pointer', 'pointer']);
|
|
40
|
+
const strategyNext = native('query', 'Qt5Xml.dll', '?nextSiblingElement@QDomNode@@QEBA?AVQDomElement@@AEBVQString@@@Z', 'pointer', ['pointer', 'pointer', 'pointer']);
|
|
41
|
+
const strategyNull = native('query', 'Qt5Xml.dll', '?isNull@QDomNode@@QEBA_NXZ', 'bool', ['pointer']);
|
|
42
|
+
const strategyAttr = native('query', 'Qt5Xml.dll', '?attribute@QDomElement@@QEBA?AVQString@@AEBV2@0@Z', 'pointer', ['pointer', 'pointer', 'pointer', 'pointer']);
|
|
43
|
+
const isVisible = native('notice', 'Qt5Widgets.dll', '?isVisible@QWidget@@QEBA_NXZ', 'bool', ['pointer']);
|
|
44
|
+
const children = native('notice', 'Qt5Core.dll', '?children@QObject@@QEBAAEBV?$QList@PEAVQObject@@@@XZ', 'pointer', ['pointer']);
|
|
45
|
+
const noticeText = native('notice', 'BaseWidget.dll', '?getText@CSimpleTextWidget@XP@@QEBA?AVQString@@XZ', 'pointer', ['pointer', 'pointer']);
|
|
46
|
+
const closeNotice = native('dismiss', 'Dialog.dll', '?closeMessage@CGlobalMessageDialog@XP@@SAXXZ', 'void', []);
|
|
47
|
+
|
|
48
|
+
function capabilities() {
|
|
49
|
+
const query = missing.gui.concat(missing.query);
|
|
50
|
+
const start = query.concat(missing.start);
|
|
51
|
+
const notice = query.concat(missing.notice);
|
|
52
|
+
const dismiss = notice.concat(missing.dismiss);
|
|
53
|
+
return {can_query: !query.length, can_start: !start.length,
|
|
54
|
+
can_read_notices: !notice.length, can_dismiss_notices: !dismiss.length,
|
|
55
|
+
missing_query: query, missing_start: start, missing_notice: notice, missing_dismiss: dismiss};
|
|
56
|
+
}
|
|
57
|
+
const typeNames = new Map();
|
|
58
|
+
function hasType(object, name) {
|
|
59
|
+
if (!typeNames.has(name)) typeNames.set(name, Memory.allocUtf8String(name));
|
|
60
|
+
return inherits(object, typeNames.get(name));
|
|
61
|
+
}
|
|
62
|
+
function getRoot(hwnd) {
|
|
63
|
+
const root = findWidget(uint64(hwnd));
|
|
64
|
+
if (root.isNull()) throw new Error('No Qt widget for target window');
|
|
65
|
+
return root;
|
|
66
|
+
}
|
|
67
|
+
let pending = null;
|
|
68
|
+
function onGui(hwnd, operation) {
|
|
69
|
+
if (missing.gui.length) throw new Error('Missing Qt GUI interfaces: ' + missing.gui.join(', '));
|
|
70
|
+
if (pending) throw new Error('An operation is already pending');
|
|
71
|
+
const thread = getThread(ptr(hwnd), NULL);
|
|
72
|
+
if (!thread) throw new Error('Target window no longer exists');
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const timer = setTimeout(() => {
|
|
75
|
+
if (pending && pending.resolve === resolve) {
|
|
76
|
+
pending = null;
|
|
77
|
+
reject(new Error('GUI operation timeout'));
|
|
78
|
+
}
|
|
79
|
+
}, 8000);
|
|
80
|
+
pending = {thread, operation, resolve, reject, timer};
|
|
81
|
+
if (!postMessage(ptr(hwnd), 0, NULL, NULL)) {
|
|
82
|
+
pending = null;
|
|
83
|
+
clearTimeout(timer);
|
|
84
|
+
reject(new Error('Cannot wake QMT GUI thread'));
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (!missing.gui.length) Interceptor.attach(dispatchAddress, {
|
|
89
|
+
onLeave() {
|
|
90
|
+
if (!pending || this.threadId !== pending.thread) return;
|
|
91
|
+
const job = pending;
|
|
92
|
+
pending = null;
|
|
93
|
+
clearTimeout(job.timer);
|
|
94
|
+
try { job.resolve({gui_thread: this.threadId, result: job.operation()}); }
|
|
95
|
+
catch (error) { job.reject(error); }
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
function strategyString(text, action) {
|
|
100
|
+
const value = Memory.alloc(Process.pointerSize);
|
|
101
|
+
stringCtor(value, Memory.allocUtf16String(text), text.length);
|
|
102
|
+
try { return action(value); } finally { stringDtor(value); }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function readString(value) {
|
|
106
|
+
const size = stringSize(value);
|
|
107
|
+
if (size < 0 || size > 65536) throw new Error('Invalid Qt string size');
|
|
108
|
+
return size ? stringData(value).readUtf16String(size) : '';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function strategyAttribute(element, name) {
|
|
112
|
+
return strategyString(name, key => strategyString('', empty => {
|
|
113
|
+
const value = Memory.alloc(Process.pointerSize);
|
|
114
|
+
strategyAttr(element, value, key, empty);
|
|
115
|
+
try { return readString(value); }
|
|
116
|
+
finally { stringDtor(value); }
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function strategySnapshot(root, checkStartup = false) {
|
|
121
|
+
const support = capabilities();
|
|
122
|
+
if (!support.can_query) throw new Error('Missing strategy query interfaces: ' + support.missing_query.join(', '));
|
|
123
|
+
if (!hasType(root, 'TC::CMainWindow')) throw new Error('QMT is not logged in');
|
|
124
|
+
const manager = strategyManager();
|
|
125
|
+
if (manager.isNull()) throw new Error('Strategy manager unavailable');
|
|
126
|
+
const doc = Memory.alloc(64), parent = Memory.alloc(64), models = [];
|
|
127
|
+
strategyDocCtor(doc);
|
|
128
|
+
try {
|
|
129
|
+
strategyString('Models', tag => strategyCreateElem(doc, parent, tag));
|
|
130
|
+
try {
|
|
131
|
+
// Serializes only into our temporary DOM; no configuration is written.
|
|
132
|
+
strategySave(manager, parent, doc);
|
|
133
|
+
strategyString('item', tag => {
|
|
134
|
+
let element = Memory.alloc(64);
|
|
135
|
+
strategyFirst(parent, element, tag);
|
|
136
|
+
try {
|
|
137
|
+
while (!strategyNull(element)) {
|
|
138
|
+
if (models.length >= 1000) throw new Error('Too many QMT models');
|
|
139
|
+
const row = {};
|
|
140
|
+
for (const key of ['id', 'name', 'account', 'm_strAccountKey', 'runMode',
|
|
141
|
+
'startupAutorun', 'eStrategyType', 'stock', 'peroid']) {
|
|
142
|
+
row[key] = strategyAttribute(element, key);
|
|
143
|
+
}
|
|
144
|
+
row.running = Boolean(strategyString(row.name, name => strategyRunning(manager, name)));
|
|
145
|
+
models.push(row);
|
|
146
|
+
const next = Memory.alloc(64);
|
|
147
|
+
strategyNext(element, next, tag);
|
|
148
|
+
strategyElemDtor(element);
|
|
149
|
+
element = next;
|
|
150
|
+
}
|
|
151
|
+
} finally { strategyElemDtor(element); }
|
|
152
|
+
});
|
|
153
|
+
} finally { strategyElemDtor(parent); }
|
|
154
|
+
} finally { strategyDocDtor(doc); }
|
|
155
|
+
return {models, capabilities: support,
|
|
156
|
+
whitelist_ready: checkStartup && strategyWhitelistReady ? Boolean(strategyWhitelistReady(manager)) : null};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
rpc.exports.strategycapabilities = capabilities;
|
|
160
|
+
rpc.exports.status = hwnd => onGui(hwnd, () => {
|
|
161
|
+
const root = getRoot(hwnd);
|
|
162
|
+
return {is_main: hasType(root, 'TC::CMainWindow'), is_login: hasType(root, 'TC::CLoginDialog'), login_identity: null};
|
|
163
|
+
});
|
|
164
|
+
rpc.exports.strategystatus = hwnd => onGui(hwnd, () => strategySnapshot(getRoot(hwnd)));
|
|
165
|
+
function childObjects(object) {
|
|
166
|
+
// Qt 5 x64 QList<QObject*>; never assume broker-specific widget offsets.
|
|
167
|
+
const data = children(object).readPointer();
|
|
168
|
+
const begin = data.add(8).readS32(), end = data.add(12).readS32();
|
|
169
|
+
if (begin < 0 || end < begin || end > 5000) throw new Error('Unexpected QList layout');
|
|
170
|
+
const result = [];
|
|
171
|
+
for (let index = begin; index < end; index++) result.push(data.add(16 + index * Process.pointerSize).readPointer());
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
function illegalIpNotice(hwnd) {
|
|
175
|
+
if (!capabilities().can_read_notices) return null;
|
|
176
|
+
const root = getRoot(hwnd);
|
|
177
|
+
if (!(hasType(root, 'XP::CGlobalMessageDialog') || hasType(root, 'CGlobalMessageDialog')) || !isVisible(root)) return null;
|
|
178
|
+
const queue = [root], seen = new Set(), matches = [];
|
|
179
|
+
while (queue.length) {
|
|
180
|
+
const object = queue.shift();
|
|
181
|
+
if (object.isNull() || seen.has(object.toString())) continue;
|
|
182
|
+
seen.add(object.toString());
|
|
183
|
+
if (seen.size > 512) throw new Error('Message widget tree too large');
|
|
184
|
+
if ((hasType(object, 'XP::CSimpleTextWidget') || hasType(object, 'CSimpleTextWidget')) && isVisible(object)) {
|
|
185
|
+
const value = Memory.alloc(Process.pointerSize);
|
|
186
|
+
noticeText(object, value);
|
|
187
|
+
let text;
|
|
188
|
+
try { text = readString(value); } finally { stringDtor(value); }
|
|
189
|
+
if (/^\[防外接\]\s*Python进程访问了网络地址\s*\[[^\]]+\]\s*[,,]\s*策略运行中止[!!]\s*$/.test(text)) matches.push(text);
|
|
190
|
+
}
|
|
191
|
+
queue.push(...childObjects(object));
|
|
192
|
+
}
|
|
193
|
+
return matches.length === 1 ? {hwnd, text: matches[0]} : null;
|
|
194
|
+
}
|
|
195
|
+
rpc.exports.illegalipnotice = hwnd => onGui(hwnd, () => illegalIpNotice(hwnd));
|
|
196
|
+
rpc.exports.dismissillegalip = (mainHwnd, hwnd, text, expected) => onGui(mainHwnd, () => {
|
|
197
|
+
if (!capabilities().can_dismiss_notices) return {outcome: 'notice_close_unavailable', closed: false};
|
|
198
|
+
const models = strategySnapshot(getRoot(mainHwnd)).models;
|
|
199
|
+
for (const target of expected) {
|
|
200
|
+
const matches = models.filter(model => model.name === target.name);
|
|
201
|
+
if (matches.length !== 1 || Object.keys(target).some(key => matches[0][key] !== target[key])) {
|
|
202
|
+
throw new Error('Strategy binding changed before warning dismissal');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (!expected.length) throw new Error('No bound strategy');
|
|
206
|
+
const current = illegalIpNotice(hwnd);
|
|
207
|
+
if (!current) return {outcome: 'already_hidden_or_changed', closed: false};
|
|
208
|
+
if (current.text !== text) return {outcome: 'message_changed', closed: false};
|
|
209
|
+
closeNotice();
|
|
210
|
+
const closed = !isVisible(getRoot(hwnd));
|
|
211
|
+
return {outcome: closed ? 'closed' : 'close_failed', closed};
|
|
212
|
+
});
|
|
213
|
+
rpc.exports.strategystart = (hwnd, expected) => onGui(hwnd, () => {
|
|
214
|
+
if (!capabilities().can_start) return {outcome: 'detection_only', requested: false};
|
|
215
|
+
// Re-read on the same GUI turn as the mutation. Cached IDs are insufficient.
|
|
216
|
+
const snapshot = strategySnapshot(getRoot(hwnd), true);
|
|
217
|
+
const matches = snapshot.models.filter(row => row.name === expected.name);
|
|
218
|
+
if (matches.length !== 1) throw new Error('No unique bound strategy');
|
|
219
|
+
const model = matches[0];
|
|
220
|
+
for (const key of ['id', 'name', 'account', 'm_strAccountKey', 'runMode',
|
|
221
|
+
'startupAutorun', 'eStrategyType', 'stock', 'peroid']) {
|
|
222
|
+
if (typeof expected[key] !== 'string' || !expected[key] || model[key] !== expected[key]) {
|
|
223
|
+
throw new Error('Strategy binding changed: ' + key);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (!/^CFQ_[A-Za-z0-9_]+$/.test(model.name) || model.startupAutorun !== '1' ||
|
|
227
|
+
model.eStrategyType !== '4' || !/^[1-9][0-9]*$/.test(model.id) || Number(model.id) > 2147483647) {
|
|
228
|
+
throw new Error('Not an eligible managed strategy');
|
|
229
|
+
}
|
|
230
|
+
if (model.running) return {outcome: 'already_running', requested: false};
|
|
231
|
+
if (!snapshot.whitelist_ready) return {outcome: 'waiting_whitelist', requested: false};
|
|
232
|
+
// Empty release-mode MSVC x64 std::string (SSO), matching the exported ABI.
|
|
233
|
+
const reason = Memory.alloc(32);
|
|
234
|
+
reason.writeByteArray(new Uint8Array(32));
|
|
235
|
+
reason.add(24).writeU64(15);
|
|
236
|
+
strategyRun(strategyManager(), Number(model.id), reason);
|
|
237
|
+
return {outcome: 'start_requested', requested: true};
|
|
238
|
+
});
|
|
239
|
+
})();
|
|
@@ -6,7 +6,6 @@ The command line defaults to inspection; --ensure/--watch enable recovery.
|
|
|
6
6
|
import argparse
|
|
7
7
|
import copy
|
|
8
8
|
import datetime
|
|
9
|
-
import hashlib
|
|
10
9
|
import json
|
|
11
10
|
import os
|
|
12
11
|
from pathlib import Path
|
|
@@ -24,17 +23,6 @@ from .qmt_strategy_deploy import (
|
|
|
24
23
|
)
|
|
25
24
|
|
|
26
25
|
|
|
27
|
-
# Verified on Guojin simulation 2.1.33.0 / Qt 5.6.2 x64. Native ABI support is
|
|
28
|
-
# deliberately explicit; another build needs its own inspection and live test.
|
|
29
|
-
SUPPORTED_FORMULA_SHA256 = {
|
|
30
|
-
"673e512bab175e068cbee5fa349e33025bf564e5f57192ff3509f87dad8b8214",
|
|
31
|
-
}
|
|
32
|
-
NOTICE_DLL_SHA256 = {
|
|
33
|
-
"Dialog.dll": "b71c3dc77cfa830e1037ac2b7f1cc10a43aabec113a9a4c77d0fa97b34cedd67",
|
|
34
|
-
"BaseWidget.dll": "31fd0313f8b2b1f55f0d367121782ec3dc941a884d59a40cfc77ae155e2e861b",
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
26
|
def visible_notice_windows(pid, main_hwnd):
|
|
39
27
|
if os.name != "nt" or not pid or not main_hwnd:
|
|
40
28
|
return []
|
|
@@ -92,18 +80,14 @@ class UnsupportedQmt(RuntimeError):
|
|
|
92
80
|
class QtStrategyBackend(QtLoginBackend):
|
|
93
81
|
def __init__(self, root):
|
|
94
82
|
directory = qmt_root(root) / "bin.x64"
|
|
95
|
-
digest = hashlib.sha256((directory / "Formula.dll").read_bytes()).hexdigest()
|
|
96
|
-
if digest not in SUPPORTED_FORMULA_SHA256:
|
|
97
|
-
raise UnsupportedQmt("QMT 策略组件版本尚未验证,未执行自动启动")
|
|
98
|
-
self.notice_supported = all(hashlib.sha256((directory / name).read_bytes()).hexdigest() == digest
|
|
99
|
-
for name, digest in NOTICE_DLL_SHA256.items())
|
|
100
83
|
extension = Path(__file__).with_name("qmt_strategy_agent.js").read_text(encoding="utf-8")
|
|
101
|
-
super().__init__(directory, agent_extension=extension)
|
|
84
|
+
super().__init__(directory, agent_extension=extension, strategy=True)
|
|
102
85
|
try:
|
|
103
86
|
if not self.already_logged_in():
|
|
104
87
|
raise QmtNotReady("QMT 尚未登录,等待主界面")
|
|
105
|
-
|
|
106
|
-
|
|
88
|
+
self.capabilities = self.api.strategycapabilities()
|
|
89
|
+
if not self.capabilities.get("can_query"):
|
|
90
|
+
raise UnsupportedQmt("QMT 缺少策略运行检测接口: " + ", ".join(self.capabilities.get("missing_query", [])))
|
|
107
91
|
except Exception:
|
|
108
92
|
self.close()
|
|
109
93
|
raise
|
|
@@ -115,7 +99,7 @@ class QtStrategyBackend(QtLoginBackend):
|
|
|
115
99
|
return self.api.strategystart(str(self.main_hwnd), expected)["result"]
|
|
116
100
|
|
|
117
101
|
def notices(self):
|
|
118
|
-
if not self.
|
|
102
|
+
if not self.capabilities.get("can_read_notices"):
|
|
119
103
|
return []
|
|
120
104
|
result = []
|
|
121
105
|
for hwnd in visible_notice_windows(self.pid, self.main_hwnd):
|
|
@@ -186,7 +170,15 @@ class QmtStrategyRecovery:
|
|
|
186
170
|
event_id=(status.get("incident") or {}).get("id"), **details))
|
|
187
171
|
|
|
188
172
|
def _notices(self, job, status, backend, expected):
|
|
189
|
-
|
|
173
|
+
try:
|
|
174
|
+
notices = backend.notices()
|
|
175
|
+
status.pop("notice_error", None)
|
|
176
|
+
except Exception as error:
|
|
177
|
+
# An optional broker-specific message widget must not disable
|
|
178
|
+
# the independently supported strategy-state query.
|
|
179
|
+
status["notice_error"] = str(error)
|
|
180
|
+
return
|
|
181
|
+
for notice in notices:
|
|
190
182
|
if expected_models(job, self.accounts_provider()) != expected or not self.job_is_current(job):
|
|
191
183
|
raise ValueError("Binding changed before warning dismissal")
|
|
192
184
|
incident = status.get("incident") or {}
|
|
@@ -249,7 +241,9 @@ class QmtStrategyRecovery:
|
|
|
249
241
|
if (status.get("pid"), status.get("generation")) != (backend.pid, job["generation"]):
|
|
250
242
|
status = {"last_checked_at": now, "next_check_at": now + self.interval,
|
|
251
243
|
"pid": backend.pid, "generation": job["generation"], "attempts": {}}
|
|
252
|
-
|
|
244
|
+
snapshot = backend.snapshot()
|
|
245
|
+
models = match_models(snapshot, expected)
|
|
246
|
+
status["capabilities"] = snapshot.get("capabilities") or {}
|
|
253
247
|
status["main_hwnd"] = getattr(backend, "main_hwnd", None)
|
|
254
248
|
if not inspect_only:
|
|
255
249
|
self._notices(job, status, backend, expected)
|
|
@@ -271,7 +265,9 @@ class QmtStrategyRecovery:
|
|
|
271
265
|
else:
|
|
272
266
|
status.pop("stable_since", None)
|
|
273
267
|
status["state"] = "stopped"
|
|
274
|
-
if not inspect_only:
|
|
268
|
+
if not inspect_only and status["capabilities"].get("can_start") is False:
|
|
269
|
+
status["state"] = "detection_only"
|
|
270
|
+
elif not inspect_only:
|
|
275
271
|
attempts = dict(status.get("attempts") or {})
|
|
276
272
|
status["attempts"] = attempts
|
|
277
273
|
if any(attempts.get(target["name"], 0) >= 5 for target in stopped):
|
|
@@ -389,8 +385,11 @@ class QmtStrategyRecoveryScheduler:
|
|
|
389
385
|
"backoff": "托管策略尚未恢复,等待冷却后重试",
|
|
390
386
|
"retry_exhausted": "托管策略连续启动失败,已暂停重试,请检查 QMT 提示",
|
|
391
387
|
}[state]
|
|
388
|
+
elif state == "detection_only":
|
|
389
|
+
target["state"] = "waiting_manual_start"
|
|
390
|
+
target["message"] = "已检测到托管策略停止,当前 QMT 缺少自动恢复接口,请手动启动策略"
|
|
392
391
|
elif state == "unsupported" and target.get("state") != "running":
|
|
393
|
-
target["message"] = recovery.get("error", "当前 QMT
|
|
392
|
+
target["message"] = recovery.get("error", "当前 QMT 缺少策略运行检测接口")
|
|
394
393
|
return status
|
|
395
394
|
|
|
396
395
|
def start(self):
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Run the actual Frida agent against synthetic Qt exports; no live QMT."""
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
from types import SimpleNamespace
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from cfquant.qmt_component_login import _verify_environment_native
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
AGENT = Path(__file__).resolve().parents[1] / 'qmt_strategy_agent.js'
|
|
14
|
+
HARNESS = r'''
|
|
15
|
+
const fs = require('fs'), vm = require('vm');
|
|
16
|
+
const options = JSON.parse(fs.readFileSync(0, 'utf8'));
|
|
17
|
+
let nextId = 1, hook, starts = 0, closes = 0, whitelistChecks = 0;
|
|
18
|
+
class Pointer {
|
|
19
|
+
constructor(value = {}, offset = 0, id = nextId++) { this.value = value; this.offset = offset; this.id = id; }
|
|
20
|
+
add(offset) { return new Pointer(this.value, this.offset + offset, this.id); }
|
|
21
|
+
isNull() { return this.value === null; }
|
|
22
|
+
toString() { return String(this.id); }
|
|
23
|
+
readPointer() {
|
|
24
|
+
if (this.value.list) return this.value.list;
|
|
25
|
+
return this.value.items[(this.offset - 16) / 8];
|
|
26
|
+
}
|
|
27
|
+
readS32() { return this.offset === 8 ? 0 : this.value.items.length; }
|
|
28
|
+
readUtf16String() { return this.value.text; }
|
|
29
|
+
writeByteArray() {}
|
|
30
|
+
writeU64() {}
|
|
31
|
+
}
|
|
32
|
+
const target = {id:'7', name:'CFQ_TEST001_LITE', account:'TEST001', m_strAccountKey:'key',
|
|
33
|
+
runMode:'1', startupAutorun:'1', eStrategyType:'4', stock:'SH000300', peroid:'86400'};
|
|
34
|
+
const models = [Object.assign({}, target, {running: options.running !== false})];
|
|
35
|
+
const main = new Pointer({types:['TC::CMainWindow'], children:[]});
|
|
36
|
+
const text = '[防外接] Python进程访问了网络地址 [192.0.2.1],策略运行中止!';
|
|
37
|
+
const content = new Pointer({types:[options.unqualified ? 'CSimpleTextWidget' : 'XP::CSimpleTextWidget'], text, children:[]});
|
|
38
|
+
const container = new Pointer({types:['QWidget'], children:[content]});
|
|
39
|
+
const notice = new Pointer({types:['XP::CGlobalMessageDialog'], children:[container]});
|
|
40
|
+
if (options.duplicate) notice.value.children.push(new Pointer({types:['XP::CSimpleTextWidget'], text, children:[]}));
|
|
41
|
+
if (options.unrelated) content.value.text = 'An unrelated message';
|
|
42
|
+
const manager = new Pointer({});
|
|
43
|
+
function call(symbol, args) {
|
|
44
|
+
const [a,b,c] = args;
|
|
45
|
+
if (symbol === 'DispatchMessageW') return;
|
|
46
|
+
if (symbol === 'GetWindowThreadProcessId') return 1;
|
|
47
|
+
if (symbol === 'PostMessageW') { setImmediate(() => hook.onLeave.call({threadId:1})); return true; }
|
|
48
|
+
if (symbol.startsWith('?find@QWidget')) return String(a) === '1' ? main : notice;
|
|
49
|
+
if (symbol.startsWith('?inherits@')) return a.value.types.includes(b.value.text);
|
|
50
|
+
if (symbol.startsWith('?isVisible@')) return a.value.visible !== false;
|
|
51
|
+
if (symbol.startsWith('??0QString')) { a.value.text = b.value.text; return a; }
|
|
52
|
+
if (symbol.startsWith('??1QString')) return;
|
|
53
|
+
if (symbol.startsWith('?size@QString')) return a.value.text.length;
|
|
54
|
+
if (symbol.startsWith('?utf16@QString')) return a;
|
|
55
|
+
if (symbol.startsWith('?instance@')) return manager;
|
|
56
|
+
if (symbol.startsWith('?saveSettings@')) { b.value.models = models; return; }
|
|
57
|
+
if (symbol.startsWith('?isFomrulaRun@')) return models.some(row => row.name === b.value.text && row.running);
|
|
58
|
+
if (symbol.startsWith('?isConnWhiteListReady@')) { whitelistChecks++; return options.whitelist !== false; }
|
|
59
|
+
if (symbol.startsWith('?runStrategy@')) { starts++; return; }
|
|
60
|
+
if (symbol.startsWith('??0QDomDocument') || symbol.startsWith('??1QDomDocument') || symbol.startsWith('??1QDomElement')) return;
|
|
61
|
+
if (symbol.startsWith('?createElement@')) return b;
|
|
62
|
+
if (symbol.startsWith('?firstChildElement@')) { b.value.models = a.value.models; b.value.index = 0; return b; }
|
|
63
|
+
if (symbol.startsWith('?nextSiblingElement@')) { b.value.models = a.value.models; b.value.index = a.value.index + 1; return b; }
|
|
64
|
+
if (symbol.startsWith('?isNull@QDomNode')) return a.value.index >= a.value.models.length;
|
|
65
|
+
if (symbol.startsWith('?attribute@')) { b.value.text = a.value.models[a.value.index][c.value.text]; return b; }
|
|
66
|
+
if (symbol.startsWith('?children@')) return new Pointer({list:new Pointer({items:a.value.children})});
|
|
67
|
+
if (symbol.startsWith('?getText@')) { b.value.text = a.value.text; return b; }
|
|
68
|
+
if (symbol.startsWith('?closeMessage@')) { closes++; notice.value.visible = false; return; }
|
|
69
|
+
throw new Error('Unimplemented test native call: ' + symbol);
|
|
70
|
+
}
|
|
71
|
+
const context = {
|
|
72
|
+
Process: {arch:'x64', pointerSize:8, findModuleByName: name => {
|
|
73
|
+
if ((options.missingModules || []).includes(name)) return null;
|
|
74
|
+
return {findExportByName: symbol => (options.missing || []).some(prefix => symbol.startsWith(prefix)) ? null : symbol};
|
|
75
|
+
}},
|
|
76
|
+
NativeFunction: function(symbol) { return (...args) => call(symbol, args); },
|
|
77
|
+
Memory: {alloc: () => new Pointer(), allocUtf8String: text => new Pointer({text}), allocUtf16String: text => new Pointer({text})},
|
|
78
|
+
Interceptor: {attach: (address, callbacks) => { hook = callbacks; }},
|
|
79
|
+
rpc: {exports:{}}, uint64: value => value, ptr: value => value, NULL:null, setTimeout, clearTimeout
|
|
80
|
+
};
|
|
81
|
+
vm.createContext(context);
|
|
82
|
+
vm.runInContext(fs.readFileSync(options.agent, 'utf8'), context);
|
|
83
|
+
(async () => {
|
|
84
|
+
const api = context.rpc.exports, result = {capabilities:api.strategycapabilities()};
|
|
85
|
+
if (result.capabilities.can_query) {
|
|
86
|
+
result.snapshot = (await api.strategystatus('1')).result;
|
|
87
|
+
if (options.tryStart) result.start = (await api.strategystart('1', Object.assign({}, target, options.expected || {}))).result;
|
|
88
|
+
if (options.notice) {
|
|
89
|
+
result.notice = (await api.illegalipnotice('2')).result;
|
|
90
|
+
if (result.notice) {
|
|
91
|
+
if (options.changed) content.value.text = text.replace('192.0.2.1','192.0.2.2');
|
|
92
|
+
result.dismiss = (await api.dismissillegalip('1','2',result.notice.text,[target])).result;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
try { await api.strategystatus('1'); } catch (error) { result.query_error = error.message; }
|
|
97
|
+
}
|
|
98
|
+
result.starts = starts; result.closes = closes; result.whitelistChecks = whitelistChecks;
|
|
99
|
+
process.stdout.write(JSON.stringify(result));
|
|
100
|
+
})().catch(error => { process.stdout.write(JSON.stringify({error:error.message,starts,closes})); });
|
|
101
|
+
'''
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def run_agent(**options):
|
|
105
|
+
node = shutil.which('node')
|
|
106
|
+
assert node, 'Node is required to validate the actual strategy agent'
|
|
107
|
+
result = subprocess.run([node, '-e', HARNESS], input=json.dumps(dict(options, agent=str(AGENT))),
|
|
108
|
+
text=True, encoding='utf-8', capture_output=True, check=True, timeout=15)
|
|
109
|
+
return json.loads(result.stdout)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@pytest.mark.parametrize('missing', [['?isConnWhiteListReady@'], ['?runStrategy@'], ['?getText@'], ['?closeMessage@']])
|
|
113
|
+
def test_optional_export_does_not_prevent_query(missing):
|
|
114
|
+
result = run_agent(missing=missing, tryStart=True, running=False)
|
|
115
|
+
assert result['snapshot']['models'][0]['running'] is False
|
|
116
|
+
assert result['capabilities']['can_query']
|
|
117
|
+
if missing[0] in ('?isConnWhiteListReady@', '?runStrategy@'):
|
|
118
|
+
assert not result['capabilities']['can_start']
|
|
119
|
+
assert result['start'] == {'outcome': 'detection_only', 'requested': False}
|
|
120
|
+
assert result['starts'] == 0
|
|
121
|
+
if missing[0] == '?isConnWhiteListReady@':
|
|
122
|
+
assert result['snapshot']['whitelist_ready'] is None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@pytest.mark.parametrize('module', ['LoginDialog.dll', 'Dialog.dll', 'BaseWidget.dll'])
|
|
126
|
+
def test_query_does_not_depend_on_login_or_notice_modules(module):
|
|
127
|
+
result = run_agent(missingModules=[module])
|
|
128
|
+
assert result['snapshot']['models'][0]['running'] is True
|
|
129
|
+
assert result['whitelistChecks'] == 0
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_missing_core_query_interface_is_explicit():
|
|
133
|
+
result = run_agent(missing=['?isFomrulaRun@'])
|
|
134
|
+
assert not result['capabilities']['can_query']
|
|
135
|
+
assert 'isFomrulaRun' in result['query_error']
|
|
136
|
+
assert result['starts'] == 0
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@pytest.mark.parametrize('running,whitelist,outcome,starts', [
|
|
140
|
+
(True, True, 'already_running', 0), (False, False, 'waiting_whitelist', 0),
|
|
141
|
+
(False, True, 'start_requested', 1),
|
|
142
|
+
])
|
|
143
|
+
def test_supported_start_keeps_native_guards(running, whitelist, outcome, starts):
|
|
144
|
+
result = run_agent(tryStart=True, running=running, whitelist=whitelist)
|
|
145
|
+
assert result['start']['outcome'] == outcome and result['starts'] == starts
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_start_rejects_changed_binding():
|
|
149
|
+
result = run_agent(tryStart=True, running=False, expected={'account':'WRONG'})
|
|
150
|
+
assert 'binding changed' in result['error'] and result['starts'] == 0
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@pytest.mark.parametrize('options,outcome', [({}, 'closed'), ({'unqualified':True}, 'closed'), ({'changed':True}, 'message_changed'),
|
|
154
|
+
({'missing':['?closeMessage@']}, 'notice_close_unavailable')])
|
|
155
|
+
def test_notice_uses_nested_widget_types_and_rechecks_text(options, outcome):
|
|
156
|
+
result = run_agent(notice=True, **options)
|
|
157
|
+
assert result['notice']['hwnd'] == '2'
|
|
158
|
+
assert result['dismiss']['outcome'] == outcome
|
|
159
|
+
assert result['closes'] == (1 if outcome == 'closed' else 0)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@pytest.mark.parametrize('options', [{'duplicate':True}, {'unrelated':True}, {'missingModules':['BaseWidget.dll']}])
|
|
163
|
+
def test_ambiguous_unrelated_or_unsupported_notice_is_not_closed(options):
|
|
164
|
+
result = run_agent(notice=True, **options)
|
|
165
|
+
assert result['notice'] is None and result['closes'] == 0
|
|
166
|
+
assert result['snapshot']['models'][0]['running'] is True
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@pytest.mark.parametrize('qt_version', [(5,6,2), (5,9,9), (5,15,2), (6,5,0)])
|
|
170
|
+
def test_strategy_environment_checks_qt_abi_without_exact_version_pin(tmp_path, monkeypatch, qt_version):
|
|
171
|
+
from cfquant import qmt_dependencies
|
|
172
|
+
data = bytearray(128)
|
|
173
|
+
data[0x3c:0x40] = (64).to_bytes(4, 'little')
|
|
174
|
+
data[68:70] = (0x8664).to_bytes(2, 'little')
|
|
175
|
+
(tmp_path / 'XtItClient.exe').write_bytes(data)
|
|
176
|
+
major, minor, patch = qt_version
|
|
177
|
+
api = SimpleNamespace(GetFileVersionInfo=lambda *args: {'FileVersionMS': (major << 16) | minor, 'FileVersionLS': patch << 16})
|
|
178
|
+
monkeypatch.setattr(qmt_dependencies, 'import_login_module', lambda name: api)
|
|
179
|
+
if major == 5:
|
|
180
|
+
_verify_environment_native(tmp_path, strategy=True)
|
|
181
|
+
else:
|
|
182
|
+
with pytest.raises(RuntimeError, match='ABI'):
|
|
183
|
+
_verify_environment_native(tmp_path, strategy=True)
|
|
184
|
+
# Login retains its existing independently validated compatibility policy.
|
|
185
|
+
if qt_version == (5,6,2):
|
|
186
|
+
_verify_environment_native(tmp_path)
|
|
187
|
+
else:
|
|
188
|
+
with pytest.raises(RuntimeError, match='5.6.2'):
|
|
189
|
+
_verify_environment_native(tmp_path)
|