cfquant 0.2.61__tar.gz → 0.2.63__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.63}/PKG-INFO +3 -1
- {cfquant-0.2.61 → cfquant-0.2.63}/README.md +2 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/management.py +32 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/qmt_component_login.py +24 -13
- cfquant-0.2.63/cfquant/qmt_native_files.py +145 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/qmt_native_worker.py +117 -12
- cfquant-0.2.63/cfquant/qmt_strategy_agent.js +242 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/qmt_strategy_deploy.py +33 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/qmt_strategy_recovery.py +139 -67
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_qmt_auto_login_integration.py +24 -1
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_qmt_component_login.py +21 -0
- cfquant-0.2.63/cfquant/tests/test_qmt_native_files.py +135 -0
- cfquant-0.2.63/cfquant/tests/test_qmt_native_reuse.py +135 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_qmt_native_worker.py +7 -0
- cfquant-0.2.63/cfquant/tests/test_qmt_strategy_capabilities.py +209 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_qmt_strategy_deploy.py +70 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_qmt_strategy_recovery.py +59 -4
- cfquant-0.2.63/cfquant/tests/test_strategy_recovery_api.py +239 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/version.py +2 -2
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant.egg-info/PKG-INFO +3 -1
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant.egg-info/SOURCES.txt +5 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant_web_server.py +42 -3
- {cfquant-0.2.61 → cfquant-0.2.63}/pyproject.toml +1 -1
- {cfquant-0.2.61 → cfquant-0.2.63}/qmt_scripts/CFQUANT_LITE.py +1 -1
- {cfquant-0.2.61 → cfquant-0.2.63}/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.63}/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.63}/web_dashboard/app.js +1 -1
- {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/index.html +2 -2
- cfquant-0.2.61/cfquant/qmt_strategy_agent.js +0 -131
- {cfquant-0.2.61 → cfquant-0.2.63}/LICENSE +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/LTtx/__init__.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/LTtx/tx/Config.txt +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/LTtx/tx/LTtx_server.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/LTtx/tx/__init__.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/LTtx/tx/tx.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/MANIFEST.in +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/__init__.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/_editable_install.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/account_routing.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/algo.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/algo_broker.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/batch_orders.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/build_info.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/callback_dispatcher.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/cftrader.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/channels.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/cli.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/client.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/config.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/diagnostics.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/flow_control.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/hub.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/level2.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/log_management.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/logging_i18n.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/normal_bridge.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/order_identity.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/order_meta.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/pipe_bridge.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/pipe_client.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/pipe_hub.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/pipe_transport.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/protocol.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/qmt_binding_defaults.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/qmt_binding_settings.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/qmt_bridge.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/qmt_component_agent.js +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/qmt_credentials.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/qmt_dependencies.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/qmt_pipe_runtime.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/qmt_strategy_package.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/qmt_strategy_runtime.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/qmt_update.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/quote_history.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/quote_leases.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/quote_service.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/remote_adapter.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/remote_management.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/remote_supervisor.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/rpc_timing.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/runtime_report.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/shared_quotes.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/stock_connect.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/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.63}/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.63}/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.63}/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.63}/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.63}/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.63}/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.63}/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.63}/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.63}/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.63}/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.63}/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.63}/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.63}/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.63}/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.63}/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.63}/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.63}/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.63}/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.63}/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.63}/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.63}/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.63}/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.63}/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.63}/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.63}/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.63}/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.63}/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.63}/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.63}/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.63}/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.63}/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.63}/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.63}/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.63}/cfquant/tests/_helpers.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/readme.md +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_account_query_timing.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_account_subscription_probe.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_advanced_parity.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_algo.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_algo_broker.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_api_tester_browser.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_api_tester_web.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_binding_live_refresh_browser.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_binding_runtime_browser.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_callback_recovery.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_callback_requests.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_cftrader.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_cftrader_tutorial.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_cftrader_web.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_credit_compat.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_dataframe_encoding.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_json_serialization.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_latency_flow_control.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_lite_account_routing.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_lite_deployment_paths.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_lite_parity.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_lite_qmt_dispatch.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_local_trading_calendar.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_log_management.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_management.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_management_batch.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_market_data_fallback.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_market_timing.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_order_identity.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_order_time_callback.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_pipe_shutdown.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_project_updater.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_python_api_reference.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_qmt_auto_login_browser.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_qmt_binding_defaults.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_qmt_binding_settings.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_qmt_dependencies.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_qmt_fileless.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_qmt_strategy_browser.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_qmt_strategy_web.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_qmt_update.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_quote_latency.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_quote_latency_encoding.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_quote_service.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_remote_management.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_remote_management_browser.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_remote_version_check.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_shared_quotes.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_shared_quotes_generated.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_startup_recovery.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_stock_connect.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_stock_connect_browser.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_subscription_congestion.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_table_conversion.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_trade_strategy_callback.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_trader_subscription_recovery.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_trading_dates.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_trading_monitor.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_trading_monitor_browser.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_tutorial_reader.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_tx_auto_import.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_tx_shutdown.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/trading_calendar.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/trading_monitor.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tx.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tx_trade_bridge.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/xtconstant.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/xtdata.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/xttrader.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/xttype.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant.egg-info/dependency_links.txt +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant.egg-info/entry_points.txt +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant.egg-info/requires.txt +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant.egg-info/top_level.txt +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/cfquant_pipe_hub.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/qmt_scripts/CFQUANT.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/qmt_scripts/__init__.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/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.63}/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.63}/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.63}/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.63}/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.63}/requirements.txt +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/setup.cfg +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/__init__.py +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/api-tester.js +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/avatars/amber-pulse.svg +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/avatars/copper-grid.svg +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/avatars/market-blue.svg +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/avatars/rose-circuit.svg +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/avatars/signal-green.svg +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/avatars/slate-wave.svg +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/avatars/teal-orbit.svg +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/avatars/violet-node.svg +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/cftrader-api-data.js +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/docs.js +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/favicon.svg +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/python-api-data.js +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/python-api.js +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/remote-management.js +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/styles.css +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/trading-monitor.js +0 -0
- {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/vendor/mermaid.min.js +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: cfquant
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.63
|
|
4
4
|
Summary: QMT local bridge and xtquant-compatible Python client.
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.8
|
|
@@ -283,6 +283,8 @@ print(result["status"], result["message"])
|
|
|
283
283
|
|
|
284
284
|
`version_info(include_remote=False)` 只查询本地版本;`updates.check(force=True)` 只检查更新。按需更新也识别同版本号的安装包变化,首次安装包哈希未知时会执行一次更新以建立记录。真正需要更新时,默认要求先关闭相关 QMT;也可显式传入 `auto_close_qmt=True` 允许自动关闭。`restart=False` 可留待调用方重启。部分 QMT 部署失败返回 `update_incomplete`,已安装但尚未重启返回 `restart_required`;执行更新失败会抛出 `ManagementError`。
|
|
285
285
|
|
|
286
|
+
外部程序复用绑定时的 `runtime` 对象,直接调用 `runtime.recover_strategy("YOUR_ACCOUNT_ID", account_type="STOCK")` 即可检测并尝试恢复已停止的 QMT 托管策略。本地托管实例由 SDK 自动读取管理凭据。也支持 `runtime.accounts.recover_strategy(...)` 和鉴权后的 `POST /api/qmt/strategies/recover`。返回值区分已提交启动、模型运行和通信确认,具体条件及示例见 [策略恢复接口](docs/第三方项目管理SDK.md#外部程序恢复-qmt-托管策略)。
|
|
287
|
+
|
|
286
288
|
模拟/实盘参数需与实际账号一致。完整的启动策略、更新接口、重复绑定行为与错误处理见 [第三方项目管理 SDK](docs/第三方项目管理SDK.md)。
|
|
287
289
|
|
|
288
290
|
## 从 `xtquant` 迁移到 `cfquant`
|
|
@@ -258,6 +258,8 @@ print(result["status"], result["message"])
|
|
|
258
258
|
|
|
259
259
|
`version_info(include_remote=False)` 只查询本地版本;`updates.check(force=True)` 只检查更新。按需更新也识别同版本号的安装包变化,首次安装包哈希未知时会执行一次更新以建立记录。真正需要更新时,默认要求先关闭相关 QMT;也可显式传入 `auto_close_qmt=True` 允许自动关闭。`restart=False` 可留待调用方重启。部分 QMT 部署失败返回 `update_incomplete`,已安装但尚未重启返回 `restart_required`;执行更新失败会抛出 `ManagementError`。
|
|
260
260
|
|
|
261
|
+
外部程序复用绑定时的 `runtime` 对象,直接调用 `runtime.recover_strategy("YOUR_ACCOUNT_ID", account_type="STOCK")` 即可检测并尝试恢复已停止的 QMT 托管策略。本地托管实例由 SDK 自动读取管理凭据。也支持 `runtime.accounts.recover_strategy(...)` 和鉴权后的 `POST /api/qmt/strategies/recover`。返回值区分已提交启动、模型运行和通信确认,具体条件及示例见 [策略恢复接口](docs/第三方项目管理SDK.md#外部程序恢复-qmt-托管策略)。
|
|
262
|
+
|
|
261
263
|
模拟/实盘参数需与实际账号一致。完整的启动策略、更新接口、重复绑定行为与错误处理见 [第三方项目管理 SDK](docs/第三方项目管理SDK.md)。
|
|
262
264
|
|
|
263
265
|
## 从 `xtquant` 迁移到 `cfquant`
|
|
@@ -314,6 +314,17 @@ class RuntimeManager:
|
|
|
314
314
|
"""Initialize the first binding; subsequent calls upsert that binding."""
|
|
315
315
|
return self.accounts.upsert(account_id, qmt_dir, _initialize=True, **options)
|
|
316
316
|
|
|
317
|
+
def recover_strategy(self, account_id=None, *, account_type=None, bridge_id=None,
|
|
318
|
+
account_key=None, timeout=180):
|
|
319
|
+
"""Recover a binding using the same managed service as initialize().
|
|
320
|
+
|
|
321
|
+
Reuse this RuntimeManager after binding accounts. Local managed services
|
|
322
|
+
automatically use home/management.key, just like account management.
|
|
323
|
+
"""
|
|
324
|
+
return self.accounts.recover_strategy(account_id, account_type=account_type,
|
|
325
|
+
bridge_id=bridge_id, account_key=account_key,
|
|
326
|
+
timeout=timeout)
|
|
327
|
+
|
|
317
328
|
def wait_ready(self, account_id, *, account_type="STOCK", bridge_id=None, timeout=120):
|
|
318
329
|
deadline = time.monotonic() + timeout
|
|
319
330
|
last = None
|
|
@@ -363,6 +374,27 @@ class AccountManager:
|
|
|
363
374
|
def status(self, timeout=None):
|
|
364
375
|
return self.runtime._request("GET", "/api/bindings/status", timeout=timeout)
|
|
365
376
|
|
|
377
|
+
def recover_strategy(self, account_id=None, *, account_type=None, bridge_id=None,
|
|
378
|
+
account_key=None, timeout=180):
|
|
379
|
+
"""Check and recover this binding's stopped managed QMT strategies.
|
|
380
|
+
|
|
381
|
+
Requires enabled strategy automation and a completed deployment. Returns
|
|
382
|
+
running/start_requested and per-target recovery states; submitting a start
|
|
383
|
+
is not confirmation that the strategy or its bridge is already running.
|
|
384
|
+
Uses the Web service, so it also works while the QMT bridge is offline.
|
|
385
|
+
"""
|
|
386
|
+
body = {key: str(value).strip() for key, value in {
|
|
387
|
+
"account_id": account_id, "account_type": account_type,
|
|
388
|
+
"bridge_id": bridge_id, "account_key": account_key,
|
|
389
|
+
}.items() if value is not None}
|
|
390
|
+
if not body.get("account_id") and not body.get("account_key"):
|
|
391
|
+
raise ValueError("account_id or account_key is required")
|
|
392
|
+
if any(not value for value in body.values()):
|
|
393
|
+
raise ValueError("account selectors must not be empty")
|
|
394
|
+
if not self.runtime.attached:
|
|
395
|
+
self.runtime.status() # Verify managed instance ownership first.
|
|
396
|
+
return self.runtime._request("POST", "/api/qmt/strategies/recover", body, timeout=timeout)
|
|
397
|
+
|
|
366
398
|
def upsert(self, account_id, qmt_dir, *, account_type="STOCK", mode="ctypes",
|
|
367
399
|
auto_start_qmt=False, deploy_strategy=True, strategy_autorun=True,
|
|
368
400
|
live=True, auto_close_qmt=False, force=False, _initialize=False, **options):
|
|
@@ -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()
|
|
@@ -301,6 +310,12 @@ class QtLoginBackend:
|
|
|
301
310
|
self.api.acknowledge(str(self.failure_hwnd))
|
|
302
311
|
self.failure_hwnd = None
|
|
303
312
|
|
|
313
|
+
def diagnostics(self):
|
|
314
|
+
return dict(minimized_before=self.minimized_before,
|
|
315
|
+
foreground_before=self.foreground_before,
|
|
316
|
+
foreground_after=self.gui.GetForegroundWindow(),
|
|
317
|
+
script_errors=list(self.messages))
|
|
318
|
+
|
|
304
319
|
def close(self):
|
|
305
320
|
try:
|
|
306
321
|
from .qmt_dependencies import import_login_module
|
|
@@ -332,17 +347,13 @@ def main():
|
|
|
332
347
|
if not 1 <= args.timeout <= 120:
|
|
333
348
|
parser.error("timeout must be between 1 and 120 seconds")
|
|
334
349
|
row, directory = select_binding(args.config, args.binding)
|
|
335
|
-
backend =
|
|
350
|
+
backend = IsolatedLoginBackend(directory, args.timeout)
|
|
336
351
|
try:
|
|
337
352
|
backend.select_account((row.get("qmt_auto_login") or {}).get("login_account", ""), replace=True)
|
|
338
353
|
def read_password():
|
|
339
354
|
return getpass.getpass("QMT password (hidden): ") if args.prompt else sys.stdin.readline().rstrip("\r\n")
|
|
340
355
|
result = auto_login(backend, read_password)
|
|
341
|
-
result.update(binding=args.binding, process_id=backend.pid,
|
|
342
|
-
minimized_before=backend.minimized_before,
|
|
343
|
-
foreground_before=backend.foreground_before,
|
|
344
|
-
foreground_after=backend.gui.GetForegroundWindow(),
|
|
345
|
-
script_errors=backend.messages,
|
|
356
|
+
result.update(binding=args.binding, process_id=backend.pid, **backend.diagnostics(),
|
|
346
357
|
input_method="Qt native methods; no keyboard/mouse input")
|
|
347
358
|
output = json.dumps(result, ensure_ascii=False, indent=2)
|
|
348
359
|
if args.report:
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Private, bounded-lifetime files for native workers; never use system TEMP."""
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import re
|
|
6
|
+
import shutil
|
|
7
|
+
import stat
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
import uuid
|
|
11
|
+
|
|
12
|
+
import psutil
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_SWEEP_LOCK = threading.Lock()
|
|
16
|
+
_LAST_SWEEP = {}
|
|
17
|
+
_WORKER_NAME = re.compile(r"worker-[0-9a-f]{32}\Z")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def worker_temp_root():
|
|
21
|
+
configured = os.environ.get("CFQUANT_FRIDA_TEMP_DIR")
|
|
22
|
+
if configured:
|
|
23
|
+
return Path(os.path.expandvars(configured)).expanduser().resolve()
|
|
24
|
+
runtime = os.environ.get("CFQUANT_RUNTIME_DIR")
|
|
25
|
+
if runtime:
|
|
26
|
+
return Path(os.path.expandvars(runtime)).expanduser().resolve() / "frida"
|
|
27
|
+
base = (os.environ.get("CFQUANT_HOME") or os.environ.get("CFQUANT_STATE_DIR")
|
|
28
|
+
or os.environ.get("CFQUANT_BASE_DIR") or str(Path(__file__).resolve().parents[1]))
|
|
29
|
+
return Path(os.path.expandvars(base)).expanduser().resolve() / "runtime" / "frida"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _process_alive(pid, created):
|
|
33
|
+
try:
|
|
34
|
+
return abs(psutil.Process(int(pid)).create_time() - float(created)) < 0.01
|
|
35
|
+
except (psutil.NoSuchProcess, ValueError, TypeError):
|
|
36
|
+
return False
|
|
37
|
+
except psutil.AccessDenied:
|
|
38
|
+
return True
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _linked(path):
|
|
42
|
+
info = path.lstat()
|
|
43
|
+
return stat.S_ISLNK(info.st_mode) or bool(getattr(info, "st_file_attributes", 0)
|
|
44
|
+
& getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0x400))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def remove_worker_directory(root, directory):
|
|
48
|
+
"""Delete only a direct owned child; never traverse a symlink or junction."""
|
|
49
|
+
root, directory = Path(root).resolve(), Path(directory)
|
|
50
|
+
try:
|
|
51
|
+
if (not _WORKER_NAME.fullmatch(directory.name) or _linked(directory)
|
|
52
|
+
or directory.resolve().parent != root):
|
|
53
|
+
return False
|
|
54
|
+
for parent, directories, files in os.walk(directory, followlinks=False):
|
|
55
|
+
if any(_linked(Path(parent) / name) for name in directories + files):
|
|
56
|
+
return False
|
|
57
|
+
# Keep the ownership marker until all possibly locked DLLs are gone.
|
|
58
|
+
# A partial rmtree must not make a crashed worker's files uncollectable.
|
|
59
|
+
marker = directory / "owner.json"
|
|
60
|
+
for child in directory.iterdir():
|
|
61
|
+
if child == marker:
|
|
62
|
+
continue
|
|
63
|
+
if child.is_dir():
|
|
64
|
+
shutil.rmtree(child)
|
|
65
|
+
else:
|
|
66
|
+
child.unlink()
|
|
67
|
+
marker.unlink(missing_ok=True)
|
|
68
|
+
directory.rmdir()
|
|
69
|
+
return True
|
|
70
|
+
except FileNotFoundError:
|
|
71
|
+
return True
|
|
72
|
+
except OSError:
|
|
73
|
+
return False # Loaded DLLs can remain locked briefly; a later sweep retries.
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def sweep_worker_directories(root, *, force=False):
|
|
77
|
+
root = Path(root).resolve()
|
|
78
|
+
now = time.time()
|
|
79
|
+
with _SWEEP_LOCK:
|
|
80
|
+
if not force and now - _LAST_SWEEP.get(str(root), 0) < 60:
|
|
81
|
+
return
|
|
82
|
+
_LAST_SWEEP[str(root)] = now
|
|
83
|
+
if not root.is_dir():
|
|
84
|
+
return
|
|
85
|
+
for directory in root.iterdir():
|
|
86
|
+
try:
|
|
87
|
+
if (not _WORKER_NAME.fullmatch(directory.name) or _linked(directory)
|
|
88
|
+
or not directory.is_dir() or now - directory.stat().st_mtime < 60):
|
|
89
|
+
continue
|
|
90
|
+
marker = directory / "owner.json"
|
|
91
|
+
if _linked(marker):
|
|
92
|
+
continue
|
|
93
|
+
owner = json.loads(marker.read_text(encoding="utf-8"))
|
|
94
|
+
if owner.get("format") != "cfquant-native-worker-v1":
|
|
95
|
+
continue
|
|
96
|
+
if not isinstance(owner.get("owner_pid"), int) or not isinstance(owner.get("owner_created"), (int, float)):
|
|
97
|
+
continue
|
|
98
|
+
child = owner.get("child_pid")
|
|
99
|
+
if child is not None and (not isinstance(child, int) or not isinstance(owner.get("child_created"), (int, float))):
|
|
100
|
+
continue
|
|
101
|
+
# The parent's identity protects the mkdir/Popen registration window.
|
|
102
|
+
if child is None:
|
|
103
|
+
alive = _process_alive(owner.get("owner_pid"), owner.get("owner_created"))
|
|
104
|
+
else:
|
|
105
|
+
alive = _process_alive(child, owner.get("child_created"))
|
|
106
|
+
if not alive:
|
|
107
|
+
remove_worker_directory(root, directory)
|
|
108
|
+
except (OSError, ValueError, TypeError, AttributeError):
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class NativeWorkerFiles:
|
|
113
|
+
def __init__(self):
|
|
114
|
+
self.root = worker_temp_root()
|
|
115
|
+
self.root.mkdir(parents=True, exist_ok=True)
|
|
116
|
+
sweep_worker_directories(self.root)
|
|
117
|
+
self.path = self.root / ("worker-" + uuid.uuid4().hex)
|
|
118
|
+
self.path.mkdir()
|
|
119
|
+
self.owner = dict(format="cfquant-native-worker-v1", owner_pid=os.getpid(),
|
|
120
|
+
owner_created=psutil.Process().create_time())
|
|
121
|
+
try:
|
|
122
|
+
self._save()
|
|
123
|
+
except Exception:
|
|
124
|
+
self.close()
|
|
125
|
+
raise
|
|
126
|
+
|
|
127
|
+
def _save(self):
|
|
128
|
+
marker = self.path / "owner.json"
|
|
129
|
+
pending = marker.with_suffix(".pending")
|
|
130
|
+
pending.write_text(json.dumps(self.owner), encoding="utf-8")
|
|
131
|
+
pending.replace(marker)
|
|
132
|
+
|
|
133
|
+
def register(self, pid):
|
|
134
|
+
try:
|
|
135
|
+
created = psutil.Process(pid).create_time()
|
|
136
|
+
except psutil.NoSuchProcess:
|
|
137
|
+
created = 0 # The protocol reader reports an immediate child crash.
|
|
138
|
+
self.owner.update(child_pid=pid, child_created=created)
|
|
139
|
+
self._save()
|
|
140
|
+
|
|
141
|
+
def environment(self):
|
|
142
|
+
return {name: str(self.path) for name in ("TEMP", "TMP", "TMPDIR")}
|
|
143
|
+
|
|
144
|
+
def close(self):
|
|
145
|
+
return remove_worker_directory(self.root, self.path)
|
|
@@ -12,9 +12,11 @@ import sys
|
|
|
12
12
|
import threading
|
|
13
13
|
import time
|
|
14
14
|
|
|
15
|
+
from .qmt_native_files import NativeWorkerFiles, sweep_worker_directories
|
|
16
|
+
|
|
15
17
|
|
|
16
18
|
LOGIN_METHODS = frozenset(("already_logged_in", "state", "select_account", "submit",
|
|
17
|
-
"fill", "wait_outcome", "acknowledge_failure"))
|
|
19
|
+
"fill", "wait_outcome", "acknowledge_failure", "diagnostics"))
|
|
18
20
|
STRATEGY_METHODS = frozenset(("snapshot", "start_strategy", "notices", "dismiss_notice"))
|
|
19
21
|
MAX_MESSAGE = 4 * 1024 * 1024
|
|
20
22
|
|
|
@@ -40,14 +42,29 @@ class NativeWorker:
|
|
|
40
42
|
self._sequence = 0
|
|
41
43
|
self._closed = False
|
|
42
44
|
self._tls_error = False
|
|
45
|
+
self.files = NativeWorkerFiles()
|
|
43
46
|
env = dict(os.environ, PYTHONIOENCODING="utf-8", PYTHONDONTWRITEBYTECODE="1")
|
|
44
|
-
self.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
env.update(self.files.environment())
|
|
48
|
+
try:
|
|
49
|
+
self.process = subprocess.Popen(
|
|
50
|
+
command or [sys.executable, "-u", "-m", "cfquant.qmt_native_worker"],
|
|
51
|
+
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
52
|
+
text=True, encoding="utf-8", errors="replace", bufsize=1, env=env,
|
|
53
|
+
cwd=str(Path(__file__).resolve().parents[1]),
|
|
54
|
+
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0) if os.name == "nt" else 0,
|
|
55
|
+
)
|
|
56
|
+
try:
|
|
57
|
+
self.files.register(self.process.pid)
|
|
58
|
+
except Exception:
|
|
59
|
+
if self.process.poll() is None:
|
|
60
|
+
self.process.kill()
|
|
61
|
+
self.process.wait(timeout=5)
|
|
62
|
+
for stream in (self.process.stdin, self.process.stdout, self.process.stderr):
|
|
63
|
+
stream.close()
|
|
64
|
+
raise
|
|
65
|
+
except Exception:
|
|
66
|
+
self.files.close()
|
|
67
|
+
raise
|
|
51
68
|
self._readers = [
|
|
52
69
|
threading.Thread(target=self._read_responses, name="qmt-native-response", daemon=True),
|
|
53
70
|
threading.Thread(target=self._read_errors, name="qmt-native-stderr", daemon=True),
|
|
@@ -154,7 +171,7 @@ class NativeWorker:
|
|
|
154
171
|
if not force and self.process.poll() is None:
|
|
155
172
|
self.process.stdin.write(json.dumps(dict(id=0, method="close")) + "\n")
|
|
156
173
|
self.process.stdin.flush()
|
|
157
|
-
self.process.wait(timeout=
|
|
174
|
+
self.process.wait(timeout=3)
|
|
158
175
|
except (OSError, ValueError, subprocess.TimeoutExpired):
|
|
159
176
|
pass
|
|
160
177
|
finally:
|
|
@@ -169,6 +186,79 @@ class NativeWorker:
|
|
|
169
186
|
thread.join(timeout=1)
|
|
170
187
|
self.process.stdout.close()
|
|
171
188
|
self.process.stderr.close()
|
|
189
|
+
self.files.close()
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class StrategyWorkerHost:
|
|
193
|
+
"""Reuse one Frida host, but detach the QMT session after every inspection."""
|
|
194
|
+
def __init__(self, worker_factory=NativeWorker):
|
|
195
|
+
self.worker_factory = worker_factory
|
|
196
|
+
self.worker = None
|
|
197
|
+
self.lock = threading.RLock()
|
|
198
|
+
self.stopped = threading.Event()
|
|
199
|
+
|
|
200
|
+
def __call__(self, directory):
|
|
201
|
+
self.lock.acquire()
|
|
202
|
+
try:
|
|
203
|
+
if self.stopped.is_set():
|
|
204
|
+
raise QmtAutomationCancelled("QMT 策略检测已停止")
|
|
205
|
+
worker = self.worker
|
|
206
|
+
if worker is not None and (worker._closed or worker.process.poll() is not None):
|
|
207
|
+
worker.close(force=True)
|
|
208
|
+
self.worker = worker = None
|
|
209
|
+
if worker is None:
|
|
210
|
+
worker = self.worker_factory("strategy", directory, timeout=15,
|
|
211
|
+
should_cancel=self.stopped.is_set)
|
|
212
|
+
self.worker = worker
|
|
213
|
+
else:
|
|
214
|
+
sweep_worker_directories(worker.files.root)
|
|
215
|
+
info = worker.call("open", kind="strategy", directory=str(directory), timeout=15)
|
|
216
|
+
worker.pid, worker.main_hwnd = info.get("pid"), info.get("main_hwnd")
|
|
217
|
+
return _StrategyWorkerLease(self, worker)
|
|
218
|
+
except BaseException:
|
|
219
|
+
self._discard()
|
|
220
|
+
self.lock.release()
|
|
221
|
+
raise
|
|
222
|
+
|
|
223
|
+
def _discard(self):
|
|
224
|
+
if self.worker is not None:
|
|
225
|
+
worker, self.worker = self.worker, None
|
|
226
|
+
worker.close(force=True)
|
|
227
|
+
|
|
228
|
+
def close(self):
|
|
229
|
+
self.stopped.set()
|
|
230
|
+
with self.lock:
|
|
231
|
+
if self.worker is not None:
|
|
232
|
+
worker, self.worker = self.worker, None
|
|
233
|
+
worker.close()
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class _StrategyWorkerLease:
|
|
237
|
+
def __init__(self, host, worker):
|
|
238
|
+
self.host, self.worker = host, worker
|
|
239
|
+
self.pid, self.main_hwnd = worker.pid, worker.main_hwnd
|
|
240
|
+
self.closed = False
|
|
241
|
+
|
|
242
|
+
def __getattr__(self, name):
|
|
243
|
+
if name not in STRATEGY_METHODS:
|
|
244
|
+
raise AttributeError(name)
|
|
245
|
+
def call(*args, **kwargs):
|
|
246
|
+
if self.closed:
|
|
247
|
+
raise QmtAutomationCancelled("QMT 策略检测会话已关闭")
|
|
248
|
+
return self.worker.call(name, *args, **kwargs)
|
|
249
|
+
return call
|
|
250
|
+
|
|
251
|
+
def close(self):
|
|
252
|
+
if self.closed:
|
|
253
|
+
return
|
|
254
|
+
self.closed = True
|
|
255
|
+
try:
|
|
256
|
+
try:
|
|
257
|
+
self.worker.call("release")
|
|
258
|
+
except Exception:
|
|
259
|
+
self.host._discard()
|
|
260
|
+
finally:
|
|
261
|
+
self.host.lock.release()
|
|
172
262
|
|
|
173
263
|
|
|
174
264
|
class IsolatedLoginBackend(NativeWorker):
|
|
@@ -232,6 +322,13 @@ def serve(factory=_open_backend):
|
|
|
232
322
|
opened = True
|
|
233
323
|
methods = LOGIN_METHODS if kwargs["kind"] == "login" else STRATEGY_METHODS
|
|
234
324
|
result = {key: getattr(backend, key, None) for key in ("pid", "main_hwnd")}
|
|
325
|
+
elif method == "release" and opened:
|
|
326
|
+
try:
|
|
327
|
+
if backend is not None:
|
|
328
|
+
backend.close()
|
|
329
|
+
finally:
|
|
330
|
+
backend, opened, methods = None, False, frozenset()
|
|
331
|
+
result = None
|
|
235
332
|
elif opened and backend is not None and method in methods:
|
|
236
333
|
result = getattr(backend, method)(*request.get("args", []), **request.get("kwargs", {}))
|
|
237
334
|
else:
|
|
@@ -244,9 +341,17 @@ def serve(factory=_open_backend):
|
|
|
244
341
|
output.flush()
|
|
245
342
|
request = None
|
|
246
343
|
finally:
|
|
247
|
-
|
|
248
|
-
backend
|
|
249
|
-
|
|
344
|
+
try:
|
|
345
|
+
if backend is not None:
|
|
346
|
+
backend.close()
|
|
347
|
+
finally:
|
|
348
|
+
try:
|
|
349
|
+
# detach() releases a target session, not the host's DLL cache.
|
|
350
|
+
frida = sys.modules.get("frida")
|
|
351
|
+
if frida is not None:
|
|
352
|
+
frida.shutdown()
|
|
353
|
+
finally:
|
|
354
|
+
output.close()
|
|
250
355
|
|
|
251
356
|
|
|
252
357
|
if __name__ == "__main__":
|