cfquant 0.2.62__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.62 → cfquant-0.2.63}/PKG-INFO +3 -1
- {cfquant-0.2.62 → cfquant-0.2.63}/README.md +2 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/management.py +32 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/qmt_component_login.py +8 -6
- cfquant-0.2.63/cfquant/qmt_native_files.py +145 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/qmt_native_worker.py +117 -12
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/qmt_strategy_agent.js +8 -5
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/qmt_strategy_deploy.py +33 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/qmt_strategy_recovery.py +115 -42
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_qmt_auto_login_integration.py +24 -1
- {cfquant-0.2.62 → 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.62 → cfquant-0.2.63}/cfquant/tests/test_qmt_native_worker.py +7 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_qmt_strategy_capabilities.py +23 -3
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_qmt_strategy_deploy.py +70 -0
- cfquant-0.2.63/cfquant/tests/test_strategy_recovery_api.py +239 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/version.py +2 -2
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant.egg-info/PKG-INFO +3 -1
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant.egg-info/SOURCES.txt +4 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant_web_server.py +42 -3
- {cfquant-0.2.62 → cfquant-0.2.63}/pyproject.toml +1 -1
- {cfquant-0.2.62 → cfquant-0.2.63}/qmt_scripts/CFQUANT_LITE.py +1 -1
- {cfquant-0.2.62 → 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.62 → 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.62 → cfquant-0.2.63}/web_dashboard/app.js +1 -1
- {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/index.html +2 -2
- {cfquant-0.2.62 → cfquant-0.2.63}/LICENSE +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/LTtx/__init__.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/LTtx/tx/Config.txt +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/LTtx/tx/LTtx_server.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/LTtx/tx/__init__.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/LTtx/tx/tx.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/MANIFEST.in +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/__init__.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/_editable_install.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/account_routing.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/algo.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/algo_broker.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/batch_orders.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/build_info.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/callback_dispatcher.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/cftrader.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/channels.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/cli.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/client.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/config.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/diagnostics.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/flow_control.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/hub.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/level2.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/log_management.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/logging_i18n.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/normal_bridge.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/order_identity.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/order_meta.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/pipe_bridge.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/pipe_client.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/pipe_hub.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/pipe_transport.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/protocol.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/qmt_binding_defaults.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/qmt_binding_settings.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/qmt_bridge.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/qmt_component_agent.js +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/qmt_credentials.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/qmt_dependencies.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/qmt_pipe_runtime.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/qmt_strategy_package.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/qmt_strategy_runtime.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/qmt_update.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/quote_history.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/quote_leases.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/quote_service.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/remote_adapter.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/remote_management.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/remote_supervisor.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/rpc_timing.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/runtime_report.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/shared_quotes.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/stock_connect.py +0 -0
- {cfquant-0.2.62 → 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.62 → 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.62 → 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.62 → 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.62 → 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.62 → 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.62 → 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.62 → 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.62 → 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.62 → 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.62 → 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.62 → 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.62 → 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.62 → 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.62 → 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.62 → 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.62 → 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.62 → 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.62 → 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.62 → 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.62 → 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.62 → 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.62 → 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.62 → 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.62 → 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.62 → 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.62 → 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.62 → 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.62 → 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.62 → 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.62 → 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.62 → 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.62 → 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.62 → 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.62 → cfquant-0.2.63}/cfquant/tests/_helpers.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/readme.md +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_account_query_timing.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_account_subscription_probe.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_advanced_parity.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_algo.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_algo_broker.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_api_tester_browser.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_api_tester_web.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_binding_live_refresh_browser.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_binding_runtime_browser.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_callback_recovery.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_callback_requests.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_cftrader.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_cftrader_tutorial.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_cftrader_web.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_credit_compat.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_dataframe_encoding.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_json_serialization.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_latency_flow_control.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_lite_account_routing.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_lite_deployment_paths.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_lite_parity.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_lite_qmt_dispatch.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_local_trading_calendar.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_log_management.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_management.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_management_batch.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_market_data_fallback.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_market_timing.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_order_identity.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_order_time_callback.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_pipe_shutdown.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_project_updater.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_python_api_reference.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_qmt_auto_login_browser.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_qmt_binding_defaults.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_qmt_binding_settings.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_qmt_dependencies.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_qmt_fileless.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_qmt_strategy_browser.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_qmt_strategy_recovery.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_qmt_strategy_web.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_qmt_update.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_quote_latency.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_quote_latency_encoding.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_quote_service.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_remote_management.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_remote_management_browser.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_remote_version_check.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_shared_quotes.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_shared_quotes_generated.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_startup_recovery.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_stock_connect.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_stock_connect_browser.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_subscription_congestion.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_table_conversion.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_trade_strategy_callback.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_trader_subscription_recovery.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_trading_dates.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_trading_monitor.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_trading_monitor_browser.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_tutorial_reader.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_tx_auto_import.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_tx_shutdown.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/trading_calendar.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/trading_monitor.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tx.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tx_trade_bridge.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/xtconstant.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/xtdata.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/xttrader.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/xttype.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant.egg-info/dependency_links.txt +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant.egg-info/entry_points.txt +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant.egg-info/requires.txt +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant.egg-info/top_level.txt +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/cfquant_pipe_hub.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/qmt_scripts/CFQUANT.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/qmt_scripts/__init__.py +0 -0
- {cfquant-0.2.62 → 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.62 → 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.62 → 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.62 → 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.62 → 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.62 → cfquant-0.2.63}/requirements.txt +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/setup.cfg +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/__init__.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/api-tester.js +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/avatars/amber-pulse.svg +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/avatars/copper-grid.svg +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/avatars/market-blue.svg +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/avatars/rose-circuit.svg +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/avatars/signal-green.svg +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/avatars/slate-wave.svg +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/avatars/teal-orbit.svg +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/avatars/violet-node.svg +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/cftrader-api-data.js +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/docs.js +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/favicon.svg +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/python-api-data.js +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/python-api.js +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/remote-management.js +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/styles.css +0 -0
- {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/trading-monitor.js +0 -0
- {cfquant-0.2.62 → 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):
|
|
@@ -310,6 +310,12 @@ class QtLoginBackend:
|
|
|
310
310
|
self.api.acknowledge(str(self.failure_hwnd))
|
|
311
311
|
self.failure_hwnd = None
|
|
312
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
|
+
|
|
313
319
|
def close(self):
|
|
314
320
|
try:
|
|
315
321
|
from .qmt_dependencies import import_login_module
|
|
@@ -341,17 +347,13 @@ def main():
|
|
|
341
347
|
if not 1 <= args.timeout <= 120:
|
|
342
348
|
parser.error("timeout must be between 1 and 120 seconds")
|
|
343
349
|
row, directory = select_binding(args.config, args.binding)
|
|
344
|
-
backend =
|
|
350
|
+
backend = IsolatedLoginBackend(directory, args.timeout)
|
|
345
351
|
try:
|
|
346
352
|
backend.select_account((row.get("qmt_auto_login") or {}).get("login_account", ""), replace=True)
|
|
347
353
|
def read_password():
|
|
348
354
|
return getpass.getpass("QMT password (hidden): ") if args.prompt else sys.stdin.readline().rstrip("\r\n")
|
|
349
355
|
result = auto_login(backend, read_password)
|
|
350
|
-
result.update(binding=args.binding, process_id=backend.pid,
|
|
351
|
-
minimized_before=backend.minimized_before,
|
|
352
|
-
foreground_before=backend.foreground_before,
|
|
353
|
-
foreground_after=backend.gui.GetForegroundWindow(),
|
|
354
|
-
script_errors=backend.messages,
|
|
356
|
+
result.update(binding=args.binding, process_id=backend.pid, **backend.diagnostics(),
|
|
355
357
|
input_method="Qt native methods; no keyboard/mouse input")
|
|
356
358
|
output = json.dumps(result, ensure_ascii=False, indent=2)
|
|
357
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__":
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// agent while the project is being upgraded.
|
|
5
5
|
(() => {
|
|
6
6
|
// Resolve query, start and notice APIs independently of the login agent.
|
|
7
|
-
const missing = {gui: [], query: [], start: [], notice: [], dismiss: []};
|
|
7
|
+
const missing = {gui: [], query: [], start: [], whitelist: [], notice: [], dismiss: []};
|
|
8
8
|
function native(group, moduleName, name, result, args) {
|
|
9
9
|
const module = Process.findModuleByName(moduleName);
|
|
10
10
|
const address = module && module.findExportByName(name);
|
|
@@ -30,7 +30,9 @@ const strategyManager = native('query', 'Formula.dll', '?instance@?$CSingletonT@
|
|
|
30
30
|
const strategySave = native('query', 'Formula.dll', '?saveSettings@CStrategyTradeManager@TC@@QEAAXAEAVQDomElement@@AEAVQDomDocument@@@Z', 'void', ['pointer', 'pointer', 'pointer']);
|
|
31
31
|
// isStrategyRun takes a request identifier; isFomrulaRun takes a formula name.
|
|
32
32
|
const strategyRunning = native('query', 'Formula.dll', '?isFomrulaRun@CStrategyTradeManager@TC@@QEAA_NAEBVQString@@@Z', 'bool', ['pointer', 'pointer']);
|
|
33
|
-
|
|
33
|
+
// Older QMT builds expose runStrategy without a separate readiness query.
|
|
34
|
+
// Use their native start path; never replace or change QMT whitelist checks.
|
|
35
|
+
const strategyWhitelistReady = native('whitelist', 'Formula.dll', '?isConnWhiteListReady@CStrategyTradeManager@TC@@QEBA_NXZ', 'bool', ['pointer']);
|
|
34
36
|
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
37
|
const strategyDocCtor = native('query', 'Qt5Xml.dll', '??0QDomDocument@@QEAA@XZ', 'pointer', ['pointer']);
|
|
36
38
|
const strategyDocDtor = native('query', 'Qt5Xml.dll', '??1QDomDocument@@QEAA@XZ', 'void', ['pointer']);
|
|
@@ -48,11 +50,12 @@ const closeNotice = native('dismiss', 'Dialog.dll', '?closeMessage@CGlobalMessag
|
|
|
48
50
|
function capabilities() {
|
|
49
51
|
const query = missing.gui.concat(missing.query);
|
|
50
52
|
const start = query.concat(missing.start);
|
|
53
|
+
const whitelist = query.concat(missing.whitelist);
|
|
51
54
|
const notice = query.concat(missing.notice);
|
|
52
55
|
const dismiss = notice.concat(missing.dismiss);
|
|
53
|
-
return {can_query: !query.length, can_start: !start.length,
|
|
56
|
+
return {can_query: !query.length, can_start: !start.length, can_check_whitelist: !whitelist.length,
|
|
54
57
|
can_read_notices: !notice.length, can_dismiss_notices: !dismiss.length,
|
|
55
|
-
missing_query: query, missing_start: start, missing_notice: notice, missing_dismiss: dismiss};
|
|
58
|
+
missing_query: query, missing_start: start, missing_whitelist: whitelist, missing_notice: notice, missing_dismiss: dismiss};
|
|
56
59
|
}
|
|
57
60
|
const typeNames = new Map();
|
|
58
61
|
function hasType(object, name) {
|
|
@@ -228,7 +231,7 @@ rpc.exports.strategystart = (hwnd, expected) => onGui(hwnd, () => {
|
|
|
228
231
|
throw new Error('Not an eligible managed strategy');
|
|
229
232
|
}
|
|
230
233
|
if (model.running) return {outcome: 'already_running', requested: false};
|
|
231
|
-
if (
|
|
234
|
+
if (snapshot.whitelist_ready === false) return {outcome: 'waiting_whitelist', requested: false};
|
|
232
235
|
// Empty release-mode MSVC x64 std::string (SSO), matching the exported ABI.
|
|
233
236
|
const reason = Memory.alloc(32);
|
|
234
237
|
reason.writeByteArray(new Uint8Array(32));
|
|
@@ -848,6 +848,39 @@ class QmtStrategyManager:
|
|
|
848
848
|
if changed:
|
|
849
849
|
self._save()
|
|
850
850
|
|
|
851
|
+
def prepare_for_start(self, directory):
|
|
852
|
+
"""Finish this terminal's offline deployment before allowing launch."""
|
|
853
|
+
root = qmt_root(directory)
|
|
854
|
+
with self.lock:
|
|
855
|
+
if self.load_error:
|
|
856
|
+
raise ValueError(self.load_error)
|
|
857
|
+
jobs = [job for job in self.jobs.values()
|
|
858
|
+
if os.path.normcase(str(qmt_root(job["root"]))) == os.path.normcase(str(root))]
|
|
859
|
+
if not jobs:
|
|
860
|
+
return
|
|
861
|
+
if self.process_checker(root):
|
|
862
|
+
raise RuntimeError("QMT 尚未完全退出,无法完成启动前策略部署")
|
|
863
|
+
try:
|
|
864
|
+
for job in jobs:
|
|
865
|
+
if job.get("enabled") and self._prepare_generation_transition(job):
|
|
866
|
+
self._control(job)
|
|
867
|
+
try:
|
|
868
|
+
self._advance(job)
|
|
869
|
+
except Exception as error:
|
|
870
|
+
count = int(job.get("deploy_retry_count") or 0) + 1
|
|
871
|
+
job["deploy_retry_count"] = count
|
|
872
|
+
job["deploy_retry_after"] = time.time() + min(10.0, 2.0 * count)
|
|
873
|
+
self._state(job, "error", str(error))
|
|
874
|
+
raise
|
|
875
|
+
if (job.get("state") not in ("configured", "waiting_import", "disabled")
|
|
876
|
+
or (job.get("enabled") and any(
|
|
877
|
+
not role.get("model_prepared") or role.get("reimport_required")
|
|
878
|
+
for role in job["roles"]))):
|
|
879
|
+
raise RuntimeError("QMT 启动前策略部署尚未完成: " +
|
|
880
|
+
(job.get("message") or job.get("state", "")))
|
|
881
|
+
finally:
|
|
882
|
+
self._save()
|
|
883
|
+
|
|
851
884
|
def _advance(self, job):
|
|
852
885
|
root = qmt_root(job["root"])
|
|
853
886
|
if self.process_checker(root):
|