cfquant 0.2.58__tar.gz → 0.2.59__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.58 → cfquant-0.2.59}/MANIFEST.in +1 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/PKG-INFO +1 -1
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/qmt_component_login.py +3 -2
- cfquant-0.2.59/cfquant/qmt_strategy_agent.js +131 -0
- cfquant-0.2.59/cfquant/qmt_strategy_recovery.py +480 -0
- cfquant-0.2.59/cfquant/tests/test_qmt_strategy_recovery.py +312 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/version.py +2 -2
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant.egg-info/PKG-INFO +1 -1
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant.egg-info/SOURCES.txt +3 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant_web_server.py +11 -1
- {cfquant-0.2.58 → cfquant-0.2.59}/pyproject.toml +2 -2
- {cfquant-0.2.58 → cfquant-0.2.59}/qmt_scripts/CFQUANT_LITE.py +1 -1
- {cfquant-0.2.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/web_dashboard/app.js +1 -1
- {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/index.html +2 -2
- {cfquant-0.2.58 → cfquant-0.2.59}/LICENSE +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/LTtx/__init__.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/LTtx/tx/Config.txt +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/LTtx/tx/LTtx_server.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/LTtx/tx/__init__.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/LTtx/tx/tx.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/README.md +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/__init__.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/_editable_install.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/account_routing.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/algo.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/algo_broker.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/batch_orders.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/build_info.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/callback_dispatcher.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/cftrader.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/channels.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/cli.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/client.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/config.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/diagnostics.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/flow_control.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/hub.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/level2.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/log_management.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/logging_i18n.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/management.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/normal_bridge.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/order_identity.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/order_meta.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/pipe_bridge.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/pipe_client.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/pipe_hub.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/pipe_transport.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/protocol.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/qmt_binding_settings.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/qmt_bridge.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/qmt_component_agent.js +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/qmt_credentials.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/qmt_dependencies.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/qmt_pipe_runtime.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/qmt_strategy_deploy.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/qmt_strategy_package.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/qmt_strategy_runtime.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/qmt_update.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/quote_history.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/quote_leases.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/quote_service.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/remote_adapter.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/remote_management.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/remote_supervisor.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/rpc_timing.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/runtime_report.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/shared_quotes.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/stock_connect.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/cfquant/tests/29_Level2/350/241/214/346/203/205/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/cfquant/tests/6_/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/cfquant/tests/_helpers.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/readme.md +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_account_query_timing.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_advanced_parity.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_algo.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_algo_broker.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_api_tester_browser.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_api_tester_web.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_binding_live_refresh_browser.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_binding_runtime_browser.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_callback_recovery.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_callback_requests.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_cftrader.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_cftrader_tutorial.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_cftrader_web.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_credit_compat.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_dataframe_encoding.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_json_serialization.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_latency_flow_control.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_lite_account_routing.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_lite_deployment_paths.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_lite_parity.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_lite_qmt_dispatch.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_local_trading_calendar.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_log_management.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_management.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_management_batch.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_market_data_fallback.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_market_timing.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_order_identity.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_order_time_callback.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_project_updater.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_python_api_reference.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_qmt_auto_login_browser.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_qmt_auto_login_integration.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_qmt_binding_settings.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_qmt_component_login.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_qmt_dependencies.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_qmt_fileless.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_qmt_strategy_browser.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_qmt_strategy_deploy.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_qmt_strategy_web.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_qmt_update.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_quote_latency.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_quote_latency_encoding.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_quote_service.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_remote_management.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_remote_management_browser.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_remote_version_check.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_shared_quotes.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_shared_quotes_generated.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_startup_recovery.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_stock_connect.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_stock_connect_browser.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_subscription_congestion.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_table_conversion.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_trade_strategy_callback.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_trader_subscription_recovery.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_trading_dates.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_trading_monitor.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_trading_monitor_browser.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_tutorial_reader.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_tx_auto_import.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_tx_shutdown.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/trading_calendar.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/trading_monitor.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tx.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tx_trade_bridge.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/xtconstant.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/xtdata.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/xttrader.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/xttype.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant.egg-info/dependency_links.txt +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant.egg-info/entry_points.txt +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant.egg-info/requires.txt +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant.egg-info/top_level.txt +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/cfquant_pipe_hub.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/qmt_scripts/CFQUANT.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/qmt_scripts/__init__.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/requirements.txt +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/setup.cfg +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/__init__.py +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/api-tester.js +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/avatars/amber-pulse.svg +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/avatars/copper-grid.svg +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/avatars/market-blue.svg +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/avatars/rose-circuit.svg +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/avatars/signal-green.svg +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/avatars/slate-wave.svg +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/avatars/teal-orbit.svg +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/avatars/violet-node.svg +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/cftrader-api-data.js +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/docs.js +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/favicon.svg +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/python-api-data.js +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/python-api.js +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/remote-management.js +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/styles.css +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/trading-monitor.js +0 -0
- {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/vendor/mermaid.min.js +0 -0
|
@@ -125,7 +125,7 @@ def select_binding(config_path, name):
|
|
|
125
125
|
|
|
126
126
|
|
|
127
127
|
class QtLoginBackend:
|
|
128
|
-
def __init__(self, directory, timeout=40, expected_pid=None):
|
|
128
|
+
def __init__(self, directory, timeout=40, expected_pid=None, *, agent_extension=""):
|
|
129
129
|
try:
|
|
130
130
|
from .qmt_dependencies import import_login_module
|
|
131
131
|
except ImportError:
|
|
@@ -178,7 +178,8 @@ class QtLoginBackend:
|
|
|
178
178
|
readiness.unload()
|
|
179
179
|
if not ready:
|
|
180
180
|
raise QmtNotReady("QMT login modules are still loading")
|
|
181
|
-
|
|
181
|
+
source = Path(__file__).with_name("qmt_component_agent.js").read_text(encoding="utf-8")
|
|
182
|
+
script = self.session.create_script(source + "\n" + agent_extension)
|
|
182
183
|
script.on("message", lambda message, data: self.messages.append("agent_error") if message.get("type") == "error" else None)
|
|
183
184
|
script.load()
|
|
184
185
|
self.script, self.api = script, script.exports_sync
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Appended to qmt_component_agent.js. All access runs on QMT's GUI thread.
|
|
2
|
+
// The Python backend checks the tested Formula.dll hash before loading this.
|
|
3
|
+
const strategyFormula = Process.getModuleByName('Formula.dll');
|
|
4
|
+
const strategyXml = Process.getModuleByName('Qt5Xml.dll');
|
|
5
|
+
const strategyManager = fn(strategyFormula, '?instance@?$CSingletonT@VCStrategyTradeManager@TC@@@utils@@SAPEAVCStrategyTradeManager@TC@@XZ', 'pointer', []);
|
|
6
|
+
const strategySave = fn(strategyFormula, '?saveSettings@CStrategyTradeManager@TC@@QEAAXAEAVQDomElement@@AEAVQDomDocument@@@Z', 'void', ['pointer', 'pointer', 'pointer']);
|
|
7
|
+
// isStrategyRun takes a request identifier; isFomrulaRun takes a formula name.
|
|
8
|
+
const strategyRunning = fn(strategyFormula, '?isFomrulaRun@CStrategyTradeManager@TC@@QEAA_NAEBVQString@@@Z', 'bool', ['pointer', 'pointer']);
|
|
9
|
+
const strategyWhitelistReady = fn(strategyFormula, '?isConnWhiteListReady@CStrategyTradeManager@TC@@QEBA_NXZ', 'bool', ['pointer']);
|
|
10
|
+
const strategyRun = fn(strategyFormula, '?runStrategy@CStrategyTradeManager@TC@@QEAAXHAEBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z', 'void', ['pointer', 'int', 'pointer']);
|
|
11
|
+
const strategyDocCtor = fn(strategyXml, '??0QDomDocument@@QEAA@XZ', 'pointer', ['pointer']);
|
|
12
|
+
const strategyDocDtor = fn(strategyXml, '??1QDomDocument@@QEAA@XZ', 'void', ['pointer']);
|
|
13
|
+
const strategyElemDtor = fn(strategyXml, '??1QDomElement@@QEAA@XZ', 'void', ['pointer']);
|
|
14
|
+
const strategyCreateElem = fn(strategyXml, '?createElement@QDomDocument@@QEAA?AVQDomElement@@AEBVQString@@@Z', 'pointer', ['pointer', 'pointer', 'pointer']);
|
|
15
|
+
const strategyFirst = fn(strategyXml, '?firstChildElement@QDomNode@@QEBA?AVQDomElement@@AEBVQString@@@Z', 'pointer', ['pointer', 'pointer', 'pointer']);
|
|
16
|
+
const strategyNext = fn(strategyXml, '?nextSiblingElement@QDomNode@@QEBA?AVQDomElement@@AEBVQString@@@Z', 'pointer', ['pointer', 'pointer', 'pointer']);
|
|
17
|
+
const strategyNull = fn(strategyXml, '?isNull@QDomNode@@QEBA_NXZ', 'bool', ['pointer']);
|
|
18
|
+
const strategyAttr = fn(strategyXml, '?attribute@QDomElement@@QEBA?AVQString@@AEBV2@0@Z', 'pointer', ['pointer', 'pointer', 'pointer', 'pointer']);
|
|
19
|
+
|
|
20
|
+
function strategyString(text, action) {
|
|
21
|
+
const value = Memory.alloc(8);
|
|
22
|
+
stringCtor(value, Memory.allocUtf16String(text), text.length);
|
|
23
|
+
try { return action(value); } finally { stringDtor(value); }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function strategyAttribute(element, name) {
|
|
27
|
+
return strategyString(name, key => strategyString('', empty => {
|
|
28
|
+
const value = Memory.alloc(8);
|
|
29
|
+
strategyAttr(element, value, key, empty);
|
|
30
|
+
try { return stringData(value).readUtf16String(stringSize(value)); }
|
|
31
|
+
finally { stringDtor(value); }
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function strategySnapshot(root) {
|
|
36
|
+
if (!hasType(root, 'TC::CMainWindow')) throw new Error('QMT is not logged in');
|
|
37
|
+
const manager = strategyManager();
|
|
38
|
+
if (manager.isNull()) throw new Error('Strategy manager unavailable');
|
|
39
|
+
const doc = Memory.alloc(64), parent = Memory.alloc(64), models = [];
|
|
40
|
+
strategyDocCtor(doc);
|
|
41
|
+
try {
|
|
42
|
+
strategyString('Models', tag => strategyCreateElem(doc, parent, tag));
|
|
43
|
+
try {
|
|
44
|
+
// Serializes only into our temporary DOM; no configuration is written.
|
|
45
|
+
strategySave(manager, parent, doc);
|
|
46
|
+
strategyString('item', tag => {
|
|
47
|
+
let element = Memory.alloc(64);
|
|
48
|
+
strategyFirst(parent, element, tag);
|
|
49
|
+
try {
|
|
50
|
+
while (!strategyNull(element)) {
|
|
51
|
+
if (models.length >= 1000) throw new Error('Too many QMT models');
|
|
52
|
+
const row = {};
|
|
53
|
+
for (const key of ['id', 'name', 'account', 'm_strAccountKey', 'runMode',
|
|
54
|
+
'startupAutorun', 'eStrategyType', 'stock', 'peroid']) {
|
|
55
|
+
row[key] = strategyAttribute(element, key);
|
|
56
|
+
}
|
|
57
|
+
row.running = Boolean(strategyString(row.name, name => strategyRunning(manager, name)));
|
|
58
|
+
models.push(row);
|
|
59
|
+
const next = Memory.alloc(64);
|
|
60
|
+
strategyNext(element, next, tag);
|
|
61
|
+
strategyElemDtor(element);
|
|
62
|
+
element = next;
|
|
63
|
+
}
|
|
64
|
+
} finally { strategyElemDtor(element); }
|
|
65
|
+
});
|
|
66
|
+
} finally { strategyElemDtor(parent); }
|
|
67
|
+
} finally { strategyDocDtor(doc); }
|
|
68
|
+
return {models, whitelist_ready: Boolean(strategyWhitelistReady(manager))};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
rpc.exports.strategystatus = hwnd => onGui(hwnd, () => strategySnapshot(getRoot(hwnd)));
|
|
72
|
+
// These two additional DLL builds are verified by the Python backend before
|
|
73
|
+
// accessing the message dialog's content widget (Dialog.dll: setContent).
|
|
74
|
+
function illegalIpNotice(hwnd) {
|
|
75
|
+
const root = getRoot(hwnd);
|
|
76
|
+
if (!hasType(root, 'XP::CGlobalMessageDialog') || !isVisible(root)) return null;
|
|
77
|
+
const content = root.add(0xe8).readPointer();
|
|
78
|
+
if (content.isNull() || !childObjects(root).some(child => child.equals(content))) {
|
|
79
|
+
throw new Error('Unexpected QMT message content widget');
|
|
80
|
+
}
|
|
81
|
+
const value = Memory.alloc(8);
|
|
82
|
+
fn(Process.getModuleByName('BaseWidget.dll'), '?getText@CSimpleTextWidget@XP@@QEBA?AVQString@@XZ',
|
|
83
|
+
'pointer', ['pointer', 'pointer'])(content, value);
|
|
84
|
+
let text;
|
|
85
|
+
try { text = stringData(value).readUtf16String(stringSize(value)); }
|
|
86
|
+
finally { stringDtor(value); }
|
|
87
|
+
if (!/^\[防外接\]\s*Python进程访问了网络地址\s*\[[^\]]+\]\s*[,,]\s*策略运行中止[!!]\s*$/.test(text)) return null;
|
|
88
|
+
return {hwnd, text};
|
|
89
|
+
}
|
|
90
|
+
rpc.exports.illegalipnotice = hwnd => onGui(hwnd, () => illegalIpNotice(hwnd));
|
|
91
|
+
rpc.exports.dismissillegalip = (mainHwnd, hwnd, text, expected) => onGui(mainHwnd, () => {
|
|
92
|
+
const models = strategySnapshot(getRoot(mainHwnd)).models;
|
|
93
|
+
for (const target of expected) {
|
|
94
|
+
const matches = models.filter(model => model.name === target.name);
|
|
95
|
+
if (matches.length !== 1 || Object.keys(target).some(key => matches[0][key] !== target[key])) {
|
|
96
|
+
throw new Error('Strategy binding changed before warning dismissal');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (!expected.length) throw new Error('No bound strategy');
|
|
100
|
+
const current = illegalIpNotice(hwnd);
|
|
101
|
+
if (!current) return {outcome: 'already_hidden_or_changed', closed: false};
|
|
102
|
+
if (current.text !== text) return {outcome: 'message_changed', closed: false};
|
|
103
|
+
fn(Process.getModuleByName('Dialog.dll'), '?closeMessage@CGlobalMessageDialog@XP@@SAXXZ', 'void', [])();
|
|
104
|
+
const closed = !isVisible(getRoot(hwnd));
|
|
105
|
+
return {outcome: closed ? 'closed' : 'close_failed', closed};
|
|
106
|
+
});
|
|
107
|
+
rpc.exports.strategystart = (hwnd, expected) => onGui(hwnd, () => {
|
|
108
|
+
// Re-read on the same GUI turn as the mutation. Cached IDs are insufficient.
|
|
109
|
+
const snapshot = strategySnapshot(getRoot(hwnd));
|
|
110
|
+
const matches = snapshot.models.filter(row => row.name === expected.name);
|
|
111
|
+
if (matches.length !== 1) throw new Error('No unique bound strategy');
|
|
112
|
+
const model = matches[0];
|
|
113
|
+
for (const key of ['id', 'name', 'account', 'm_strAccountKey', 'runMode',
|
|
114
|
+
'startupAutorun', 'eStrategyType', 'stock', 'peroid']) {
|
|
115
|
+
if (typeof expected[key] !== 'string' || !expected[key] || model[key] !== expected[key]) {
|
|
116
|
+
throw new Error('Strategy binding changed: ' + key);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (!/^CFQ_[A-Za-z0-9_]+$/.test(model.name) || model.startupAutorun !== '1' ||
|
|
120
|
+
model.eStrategyType !== '4' || !/^[1-9][0-9]*$/.test(model.id) || Number(model.id) > 2147483647) {
|
|
121
|
+
throw new Error('Not an eligible managed strategy');
|
|
122
|
+
}
|
|
123
|
+
if (model.running) return {outcome: 'already_running', requested: false};
|
|
124
|
+
if (!snapshot.whitelist_ready) return {outcome: 'waiting_whitelist', requested: false};
|
|
125
|
+
// Empty MSVC x64 release std::string (SSO). Allowed only on the tested build.
|
|
126
|
+
const reason = Memory.alloc(32);
|
|
127
|
+
reason.writeByteArray(new Uint8Array(32));
|
|
128
|
+
reason.add(24).writeU64(15);
|
|
129
|
+
strategyRun(strategyManager(), Number(model.id), reason);
|
|
130
|
+
return {outcome: 'start_requested', requested: true};
|
|
131
|
+
});
|
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
"""Recover the current binding's stopped QMT models through native Qt methods.
|
|
2
|
+
|
|
3
|
+
No mouse/keyboard events, order calls, whitelist changes, or terminal restarts.
|
|
4
|
+
The command line defaults to inspection; --ensure/--watch enable recovery.
|
|
5
|
+
"""
|
|
6
|
+
import argparse
|
|
7
|
+
import copy
|
|
8
|
+
import datetime
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
import sys
|
|
14
|
+
import threading
|
|
15
|
+
import time
|
|
16
|
+
import uuid
|
|
17
|
+
|
|
18
|
+
from .qmt_component_login import QtLoginBackend, QmtNotReady
|
|
19
|
+
from .qmt_strategy_deploy import (
|
|
20
|
+
_role_stock, _strategy_slot_name, account_qmt_roots,
|
|
21
|
+
_write_json, _digest, PipeStrategyController, normalize_strategy_mode, normalize_strategy_settings, qmt_root,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Verified on Guojin simulation 2.1.33.0 / Qt 5.6.2 x64. Native ABI support is
|
|
26
|
+
# deliberately explicit; another build needs its own inspection and live test.
|
|
27
|
+
SUPPORTED_FORMULA_SHA256 = {
|
|
28
|
+
"673e512bab175e068cbee5fa349e33025bf564e5f57192ff3509f87dad8b8214",
|
|
29
|
+
}
|
|
30
|
+
NOTICE_DLL_SHA256 = {
|
|
31
|
+
"Dialog.dll": "b71c3dc77cfa830e1037ac2b7f1cc10a43aabec113a9a4c77d0fa97b34cedd67",
|
|
32
|
+
"BaseWidget.dll": "31fd0313f8b2b1f55f0d367121782ec3dc941a884d59a40cfc77ae155e2e861b",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def visible_notice_windows(pid, main_hwnd):
|
|
37
|
+
if os.name != "nt" or not pid or not main_hwnd:
|
|
38
|
+
return []
|
|
39
|
+
import win32gui
|
|
40
|
+
import win32process
|
|
41
|
+
result = []
|
|
42
|
+
def visit(hwnd, _):
|
|
43
|
+
if (hwnd != main_hwnd and win32gui.IsWindowVisible(hwnd)
|
|
44
|
+
and win32process.GetWindowThreadProcessId(hwnd)[1] == pid
|
|
45
|
+
and win32gui.GetClassName(hwnd).startswith("Qt")):
|
|
46
|
+
result.append(hwnd)
|
|
47
|
+
win32gui.EnumWindows(visit, None)
|
|
48
|
+
return result
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class RecoveryJournal:
|
|
52
|
+
"""Append UTF-8 JSON lines; flush to disk before acknowledging a warning."""
|
|
53
|
+
def __init__(self, directory):
|
|
54
|
+
self.directory = Path(directory)
|
|
55
|
+
self.lock = threading.Lock()
|
|
56
|
+
|
|
57
|
+
def __call__(self, event):
|
|
58
|
+
stamp = datetime.datetime.now().astimezone()
|
|
59
|
+
event = dict(event, timestamp=stamp.isoformat(timespec="milliseconds"))
|
|
60
|
+
with self.lock:
|
|
61
|
+
self.directory.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
path = self.directory / (stamp.strftime("%Y-%m-%d") + ".jsonl")
|
|
63
|
+
with path.open("a", encoding="utf-8") as stream:
|
|
64
|
+
stream.write(json.dumps(event, ensure_ascii=False) + "\n")
|
|
65
|
+
stream.flush()
|
|
66
|
+
os.fsync(stream.fileno())
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def confirm_recovery_bridge(job, pid):
|
|
70
|
+
controller = PipeStrategyController()
|
|
71
|
+
desired = dict(enabled=True, mode=job["mode"], generation=job["generation"],
|
|
72
|
+
runtime_key=_digest([os.path.normcase(job["root"]), job["account_id"]])[:24])
|
|
73
|
+
try:
|
|
74
|
+
for role in job["roles"]:
|
|
75
|
+
reply = controller(role["identity"], desired)
|
|
76
|
+
if (reply.get("state") != "running" or reply.get("pid") != pid
|
|
77
|
+
or reply.get("generation") != job["generation"]
|
|
78
|
+
or reply.get("role") != role["role"]
|
|
79
|
+
or reply.get("runtime_key") != desired["runtime_key"]):
|
|
80
|
+
return False
|
|
81
|
+
return True
|
|
82
|
+
finally:
|
|
83
|
+
controller.close()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class UnsupportedQmt(RuntimeError):
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class QtStrategyBackend(QtLoginBackend):
|
|
91
|
+
def __init__(self, root):
|
|
92
|
+
directory = qmt_root(root) / "bin.x64"
|
|
93
|
+
digest = hashlib.sha256((directory / "Formula.dll").read_bytes()).hexdigest()
|
|
94
|
+
if digest not in SUPPORTED_FORMULA_SHA256:
|
|
95
|
+
raise UnsupportedQmt("QMT 策略组件版本尚未验证,未执行自动启动")
|
|
96
|
+
self.notice_supported = all(hashlib.sha256((directory / name).read_bytes()).hexdigest() == digest
|
|
97
|
+
for name, digest in NOTICE_DLL_SHA256.items())
|
|
98
|
+
extension = Path(__file__).with_name("qmt_strategy_agent.js").read_text(encoding="utf-8")
|
|
99
|
+
super().__init__(directory, agent_extension=extension)
|
|
100
|
+
try:
|
|
101
|
+
if not self.already_logged_in():
|
|
102
|
+
raise QmtNotReady("QMT 尚未登录,等待主界面")
|
|
103
|
+
if "strategystatus" not in self.script.list_exports_sync():
|
|
104
|
+
raise UnsupportedQmt("QMT 策略组件接口不可用")
|
|
105
|
+
except Exception:
|
|
106
|
+
self.close()
|
|
107
|
+
raise
|
|
108
|
+
|
|
109
|
+
def snapshot(self):
|
|
110
|
+
return self.api.strategystatus(str(self.main_hwnd))["result"]
|
|
111
|
+
|
|
112
|
+
def start_strategy(self, expected):
|
|
113
|
+
return self.api.strategystart(str(self.main_hwnd), expected)["result"]
|
|
114
|
+
|
|
115
|
+
def notices(self):
|
|
116
|
+
if not self.notice_supported:
|
|
117
|
+
return []
|
|
118
|
+
result = []
|
|
119
|
+
for hwnd in visible_notice_windows(self.pid, self.main_hwnd):
|
|
120
|
+
notice = self.api.illegalipnotice(str(hwnd))["result"]
|
|
121
|
+
if notice:
|
|
122
|
+
result.append(notice)
|
|
123
|
+
return result
|
|
124
|
+
|
|
125
|
+
def dismiss_notice(self, notice, expected):
|
|
126
|
+
return self.api.dismissillegalip(str(self.main_hwnd), notice["hwnd"], notice["text"], expected)["result"]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def expected_models(job, accounts):
|
|
130
|
+
"""Build an exact allowlist from the CURRENT binding and deployed job."""
|
|
131
|
+
row = accounts.get(job.get("account_key")) or {}
|
|
132
|
+
settings = normalize_strategy_settings(row.get("qmt_strategy"))
|
|
133
|
+
if (not row or not row.get("enabled", True) or not job.get("enabled")
|
|
134
|
+
or not settings["enabled"] or not settings["autorun"]
|
|
135
|
+
or str(row.get("account_id")) != job.get("account_id")
|
|
136
|
+
or normalize_strategy_mode(row.get("mode")) != job.get("mode")
|
|
137
|
+
or os.path.normcase(str(qmt_root(job["root"]))) not in account_qmt_roots(row)
|
|
138
|
+
or settings != job.get("settings")
|
|
139
|
+
or job.get("control_generation") or job.get("accepted_generations")):
|
|
140
|
+
return []
|
|
141
|
+
expected = []
|
|
142
|
+
for role in job.get("roles", []):
|
|
143
|
+
name = _strategy_slot_name(job["account_id"], job["mode"], role["role"])
|
|
144
|
+
if (role.get("name") != name or not role.get("model_prepared")
|
|
145
|
+
or role.get("reimport_required") or not role.get("model_id")
|
|
146
|
+
or not role.get("resolved_account_key")):
|
|
147
|
+
return []
|
|
148
|
+
expected.append({
|
|
149
|
+
"id": str(role["model_id"]), "name": name, "account": job["account_id"],
|
|
150
|
+
"m_strAccountKey": role["resolved_account_key"],
|
|
151
|
+
"runMode": "1" if settings["live"] else "0", "startupAutorun": "1",
|
|
152
|
+
"eStrategyType": "4", "stock": _role_stock(settings, role["role"]),
|
|
153
|
+
"peroid": str(settings["period"]),
|
|
154
|
+
})
|
|
155
|
+
return expected
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def match_models(snapshot, expected):
|
|
159
|
+
matched = []
|
|
160
|
+
for target in expected:
|
|
161
|
+
candidates = [item for item in snapshot["models"] if item.get("name") == target["name"]]
|
|
162
|
+
if len(candidates) != 1 or any(candidates[0].get(k) != v for k, v in target.items()):
|
|
163
|
+
raise ValueError("QMT 内存策略与当前绑定不一致: " + target["name"])
|
|
164
|
+
matched.append(candidates[0])
|
|
165
|
+
return matched
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class QmtStrategyRecovery:
|
|
169
|
+
def __init__(self, accounts_provider, *, backend_factory=QtStrategyBackend,
|
|
170
|
+
job_is_current=lambda job: True, clock=time.time, interval=20.0,
|
|
171
|
+
notice_probe=visible_notice_windows, event_sink=None, bridge_verifier=confirm_recovery_bridge):
|
|
172
|
+
self.accounts_provider = accounts_provider
|
|
173
|
+
self.backend_factory = backend_factory
|
|
174
|
+
self.job_is_current = job_is_current
|
|
175
|
+
self.clock = clock
|
|
176
|
+
self.interval = interval
|
|
177
|
+
self.notice_probe = notice_probe
|
|
178
|
+
self.event_sink = event_sink or RecoveryJournal(Path("runtime/logs/qmt_strategy_recovery"))
|
|
179
|
+
self.bridge_verifier = bridge_verifier
|
|
180
|
+
|
|
181
|
+
def _event(self, job, status, event, **details):
|
|
182
|
+
self.event_sink(dict(event=event, account_id=job["account_id"], account_key=job["account_key"],
|
|
183
|
+
mode=job["mode"], pid=status.get("pid"), generation=job["generation"],
|
|
184
|
+
event_id=(status.get("incident") or {}).get("id"), **details))
|
|
185
|
+
|
|
186
|
+
def _notices(self, job, status, backend, expected):
|
|
187
|
+
for notice in backend.notices():
|
|
188
|
+
if expected_models(job, self.accounts_provider()) != expected or not self.job_is_current(job):
|
|
189
|
+
raise ValueError("Binding changed before warning dismissal")
|
|
190
|
+
incident = status.get("incident") or {}
|
|
191
|
+
if (incident.get("text") != notice["text"] or incident.get("hwnd") != notice["hwnd"]
|
|
192
|
+
or incident.get("dismissed") or incident.get("handled")):
|
|
193
|
+
incident = dict(id=uuid.uuid4().hex, text=notice["text"], hwnd=notice["hwnd"], detected_at=self.clock())
|
|
194
|
+
# Detection must be durably recorded before dismissing the popup.
|
|
195
|
+
status["incident"] = incident
|
|
196
|
+
if not incident.get("recorded"):
|
|
197
|
+
self._event(job, status, "illegal_ip_detected", message=notice["text"])
|
|
198
|
+
incident["recorded"] = True
|
|
199
|
+
outcome = backend.dismiss_notice(notice, expected)
|
|
200
|
+
self._event(job, status, "illegal_ip_popup_result", **outcome)
|
|
201
|
+
incident["dismissed"] = outcome.get("closed") is True
|
|
202
|
+
incident["dismissal"] = outcome["outcome"]
|
|
203
|
+
status["next_check_at"] = self.clock() + 1
|
|
204
|
+
|
|
205
|
+
def _confirm_incident(self, job, status, backend, expected):
|
|
206
|
+
incident = status.get("incident") or {}
|
|
207
|
+
if not incident or incident.get("handled"):
|
|
208
|
+
return
|
|
209
|
+
status["next_check_at"] = self.clock() + (1 if self.clock() - incident["detected_at"] < 60 else self.interval)
|
|
210
|
+
if expected_models(job, self.accounts_provider()) != expected or not self.job_is_current(job):
|
|
211
|
+
return
|
|
212
|
+
models = match_models(backend.snapshot(), expected)
|
|
213
|
+
if not all(model["running"] for model in models):
|
|
214
|
+
return
|
|
215
|
+
try:
|
|
216
|
+
confirmed = self.bridge_verifier(job, backend.pid)
|
|
217
|
+
except Exception as error:
|
|
218
|
+
status["bridge_error"] = str(error)
|
|
219
|
+
return
|
|
220
|
+
status["bridge_confirmed"] = confirmed
|
|
221
|
+
if confirmed:
|
|
222
|
+
status.pop("bridge_error", None)
|
|
223
|
+
if confirmed and incident.get("dismissed"):
|
|
224
|
+
self._event(job, status, "illegal_ip_handled", message="非法 IP 弹窗已关闭,绑定策略及通信恢复成功",
|
|
225
|
+
strategies=[model["name"] for model in models], popup_closed=True, bridge_confirmed=True)
|
|
226
|
+
incident["handled"] = True
|
|
227
|
+
status.update(state="running", strategies=[{"name": m["name"], "running": True} for m in models])
|
|
228
|
+
status["next_check_at"] = self.clock() + self.interval
|
|
229
|
+
|
|
230
|
+
def __call__(self, job, *, inspect_only=False):
|
|
231
|
+
now = self.clock()
|
|
232
|
+
expected = expected_models(job, self.accounts_provider())
|
|
233
|
+
if not expected or not self.job_is_current(job):
|
|
234
|
+
job["recovery"] = {"state": "ineligible", "last_checked_at": now}
|
|
235
|
+
return job["recovery"]
|
|
236
|
+
previous = job.get("recovery") or {}
|
|
237
|
+
if (not inspect_only and now < previous.get("next_check_at", 0)
|
|
238
|
+
and not self.notice_probe(previous.get("pid"), previous.get("main_hwnd"))):
|
|
239
|
+
return previous
|
|
240
|
+
status = dict(previous, last_checked_at=now, next_check_at=now + self.interval)
|
|
241
|
+
status.pop("error", None)
|
|
242
|
+
backend = None
|
|
243
|
+
try:
|
|
244
|
+
backend = self.backend_factory(job["root"])
|
|
245
|
+
if (status.get("pid"), status.get("generation")) != (backend.pid, job["generation"]):
|
|
246
|
+
status = {"last_checked_at": now, "next_check_at": now + self.interval,
|
|
247
|
+
"pid": backend.pid, "generation": job["generation"], "attempts": {}}
|
|
248
|
+
models = match_models(backend.snapshot(), expected)
|
|
249
|
+
status["main_hwnd"] = getattr(backend, "main_hwnd", None)
|
|
250
|
+
if not inspect_only:
|
|
251
|
+
self._notices(job, status, backend, expected)
|
|
252
|
+
status["strategies"] = [{"name": item["name"], "running": item["running"]} for item in models]
|
|
253
|
+
status["bridge_confirmed"] = all(item["running"] for item in models) and all(
|
|
254
|
+
(role.get("control_sync") or {}).get("pid") == backend.pid
|
|
255
|
+
and (role.get("control_sync") or {}).get("state") == "running"
|
|
256
|
+
and 0 <= now - (role.get("control_sync") or {}).get("last_success_at", 0) < 10
|
|
257
|
+
for role in job["roles"])
|
|
258
|
+
stopped = [target for target, model in zip(expected, models) if not model["running"]]
|
|
259
|
+
if not stopped:
|
|
260
|
+
status["state"] = "running"
|
|
261
|
+
if status["bridge_confirmed"]:
|
|
262
|
+
status.setdefault("stable_since", now)
|
|
263
|
+
if now - status["stable_since"] >= 300:
|
|
264
|
+
status["attempts"] = {}
|
|
265
|
+
else:
|
|
266
|
+
status.pop("stable_since", None)
|
|
267
|
+
else:
|
|
268
|
+
status.pop("stable_since", None)
|
|
269
|
+
status["state"] = "stopped"
|
|
270
|
+
if not inspect_only:
|
|
271
|
+
attempts = dict(status.get("attempts") or {})
|
|
272
|
+
status["attempts"] = attempts
|
|
273
|
+
if any(attempts.get(target["name"], 0) >= 5 for target in stopped):
|
|
274
|
+
status["state"] = "retry_exhausted"
|
|
275
|
+
elif now < status.get("retry_after", 0):
|
|
276
|
+
status["state"] = "backoff"
|
|
277
|
+
else:
|
|
278
|
+
for target in stopped:
|
|
279
|
+
# Re-check the binding after attach/observation and before each start.
|
|
280
|
+
if (expected_models(job, self.accounts_provider()) != expected
|
|
281
|
+
or not self.job_is_current(job)):
|
|
282
|
+
status["state"] = "binding_changed"
|
|
283
|
+
break
|
|
284
|
+
count = attempts.get(target["name"], 0) + 1
|
|
285
|
+
attempts[target["name"]] = count
|
|
286
|
+
status["retry_after"] = now + min(300, 60 * 2 ** (count - 1))
|
|
287
|
+
# Reserve the attempt before a call that may time out ambiguously.
|
|
288
|
+
job["recovery"] = status
|
|
289
|
+
outcome = backend.start_strategy(target)
|
|
290
|
+
status["state"] = outcome["outcome"]
|
|
291
|
+
if outcome.get("requested"):
|
|
292
|
+
status["last_started_at"] = now
|
|
293
|
+
self._event(job, status, "strategy_start_requested", strategy=target["name"])
|
|
294
|
+
else:
|
|
295
|
+
attempts[target["name"]] -= 1
|
|
296
|
+
status["retry_after"] = now
|
|
297
|
+
if not inspect_only:
|
|
298
|
+
self._confirm_incident(job, status, backend, expected)
|
|
299
|
+
if status.get("state") in ("retry_exhausted", "backoff") and status.get("incident"):
|
|
300
|
+
if status.get("last_logged_state") != status["state"]:
|
|
301
|
+
self._event(job, status, "illegal_ip_recovery_pending", state=status["state"])
|
|
302
|
+
status["last_logged_state"] = status["state"]
|
|
303
|
+
except Exception as error:
|
|
304
|
+
status.update(state="unsupported" if isinstance(error, UnsupportedQmt) else "unavailable",
|
|
305
|
+
error=str(error), next_check_at=now + max(self.interval, 60))
|
|
306
|
+
if status.get("incident") and status.get("last_logged_error") != str(error):
|
|
307
|
+
try:
|
|
308
|
+
self._event(job, status, "illegal_ip_processing_failed", error=str(error))
|
|
309
|
+
status["last_logged_error"] = str(error)
|
|
310
|
+
except OSError:
|
|
311
|
+
pass # Preserve the original journal failure in status; never claim success.
|
|
312
|
+
finally:
|
|
313
|
+
if backend is not None:
|
|
314
|
+
backend.close()
|
|
315
|
+
job["recovery"] = status
|
|
316
|
+
return status
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class QmtStrategyRecoveryScheduler:
|
|
320
|
+
"""Separate worker: native GUI waits must not delay the Pipe control loop."""
|
|
321
|
+
def __init__(self, manager, accounts_provider, report_path, interval=20.0):
|
|
322
|
+
self.manager = manager
|
|
323
|
+
self.accounts_provider = accounts_provider
|
|
324
|
+
self.report_path = Path(report_path)
|
|
325
|
+
self.interval = interval
|
|
326
|
+
self.stop_event = threading.Event()
|
|
327
|
+
self.thread = None
|
|
328
|
+
self.lock = threading.RLock()
|
|
329
|
+
self.states = {}
|
|
330
|
+
try:
|
|
331
|
+
self.states = json.loads(self.report_path.read_text(encoding="utf-8")).get("targets", {})
|
|
332
|
+
except (OSError, ValueError):
|
|
333
|
+
pass
|
|
334
|
+
self.recovery = QmtStrategyRecovery(accounts_provider, job_is_current=self._current, interval=interval,
|
|
335
|
+
event_sink=RecoveryJournal(self.report_path.parent.parent / "logs/qmt_strategy_recovery"))
|
|
336
|
+
|
|
337
|
+
def _current(self, job):
|
|
338
|
+
if self.stop_event.is_set():
|
|
339
|
+
return False
|
|
340
|
+
with self.manager.lock:
|
|
341
|
+
accounts = self.accounts_provider()
|
|
342
|
+
return any(item.get("account_key") == job["account_key"]
|
|
343
|
+
and item.get("root") == job["root"]
|
|
344
|
+
and item.get("generation") == job["generation"]
|
|
345
|
+
and expected_models(item, accounts) == expected_models(job, accounts)
|
|
346
|
+
for item in self.manager.jobs.values())
|
|
347
|
+
|
|
348
|
+
def run_once(self):
|
|
349
|
+
with self.manager.lock:
|
|
350
|
+
jobs = copy.deepcopy(self.manager.jobs)
|
|
351
|
+
with self.lock:
|
|
352
|
+
states = copy.deepcopy(self.states)
|
|
353
|
+
result = {}
|
|
354
|
+
for key, job in jobs.items():
|
|
355
|
+
if self.stop_event.is_set():
|
|
356
|
+
break
|
|
357
|
+
job["recovery"] = states.get(key, {})
|
|
358
|
+
try:
|
|
359
|
+
result[key] = self.recovery(job)
|
|
360
|
+
except Exception as error:
|
|
361
|
+
result[key] = dict(job["recovery"], state="unavailable", error=str(error))
|
|
362
|
+
with self.lock:
|
|
363
|
+
self.states = result
|
|
364
|
+
_write_json(self.report_path, {"checked_at": time.time(), "targets": result})
|
|
365
|
+
|
|
366
|
+
def decorate(self, account_key, status):
|
|
367
|
+
status = copy.deepcopy(status)
|
|
368
|
+
with self.manager.lock:
|
|
369
|
+
keys = {job["root"]: (key, job.get("generation")) for key, job in self.manager.jobs.items()
|
|
370
|
+
if job.get("account_key") == account_key}
|
|
371
|
+
with self.lock:
|
|
372
|
+
for target in status.get("targets", []):
|
|
373
|
+
key, generation = keys.get(target.get("root"), (None, None))
|
|
374
|
+
recovery = copy.deepcopy(self.states.get(key, {}))
|
|
375
|
+
if (recovery.get("generation") not in (None, generation)
|
|
376
|
+
or time.time() - recovery.get("last_checked_at", 0) > 120):
|
|
377
|
+
recovery = {}
|
|
378
|
+
target["recovery"] = recovery
|
|
379
|
+
state = recovery.get("state")
|
|
380
|
+
if state in ("start_requested", "backoff", "retry_exhausted"):
|
|
381
|
+
target["state"] = "waiting_start"
|
|
382
|
+
target["message"] = {
|
|
383
|
+
"start_requested": "已检测到托管策略停止,已直接提交启动,等待通道恢复",
|
|
384
|
+
"backoff": "托管策略尚未恢复,等待冷却后重试",
|
|
385
|
+
"retry_exhausted": "托管策略连续启动失败,已暂停重试,请检查 QMT 提示",
|
|
386
|
+
}[state]
|
|
387
|
+
elif state == "unsupported" and target.get("state") != "running":
|
|
388
|
+
target["message"] = recovery.get("error", "当前 QMT 版本尚未验证策略自动恢复")
|
|
389
|
+
return status
|
|
390
|
+
|
|
391
|
+
def start(self):
|
|
392
|
+
if self.thread and self.thread.is_alive():
|
|
393
|
+
return
|
|
394
|
+
self.stop_event.clear()
|
|
395
|
+
def run():
|
|
396
|
+
while not self.stop_event.is_set():
|
|
397
|
+
try:
|
|
398
|
+
self.run_once()
|
|
399
|
+
except Exception:
|
|
400
|
+
pass # Transient config/status IO failures retry on the next interval.
|
|
401
|
+
self.stop_event.wait(1.0)
|
|
402
|
+
self.thread = threading.Thread(target=run, name="qmt-strategy-recovery", daemon=True)
|
|
403
|
+
self.thread.start()
|
|
404
|
+
|
|
405
|
+
def close(self):
|
|
406
|
+
self.stop_event.set()
|
|
407
|
+
if self.thread:
|
|
408
|
+
self.thread.join(timeout=2)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def main():
|
|
412
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
413
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
414
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
415
|
+
parser.add_argument("--account-id", required=True)
|
|
416
|
+
parser.add_argument("--config", type=Path, default=Path("runtime/config/cfquant_web_config.json"))
|
|
417
|
+
parser.add_argument("--jobs", type=Path, default=Path("runtime/config/qmt_strategy_jobs.json"))
|
|
418
|
+
action = parser.add_mutually_exclusive_group()
|
|
419
|
+
action.add_argument("--ensure", action="store_true")
|
|
420
|
+
action.add_argument("--watch", action="store_true")
|
|
421
|
+
parser.add_argument("--report", type=Path)
|
|
422
|
+
parser.add_argument("--parent-pid", type=int, help="Stop the temporary watcher when this Web process exits")
|
|
423
|
+
args = parser.parse_args()
|
|
424
|
+
|
|
425
|
+
def accounts():
|
|
426
|
+
return json.loads(args.config.read_text(encoding="utf-8"))["account_configs"]
|
|
427
|
+
|
|
428
|
+
def jobs():
|
|
429
|
+
return json.loads(args.jobs.read_text(encoding="utf-8"))["jobs"]
|
|
430
|
+
|
|
431
|
+
def current(job):
|
|
432
|
+
return any(item.get("account_key") == job["account_key"]
|
|
433
|
+
and item.get("root") == job["root"]
|
|
434
|
+
and item.get("generation") == job["generation"]
|
|
435
|
+
and item.get("enabled")
|
|
436
|
+
and expected_models(item, accounts()) == expected_models(job, accounts())
|
|
437
|
+
for item in jobs().values())
|
|
438
|
+
|
|
439
|
+
parent = None
|
|
440
|
+
if args.parent_pid:
|
|
441
|
+
import psutil
|
|
442
|
+
parent = psutil.Process(args.parent_pid)
|
|
443
|
+
recovery = QmtStrategyRecovery(accounts, job_is_current=lambda job: (
|
|
444
|
+
(parent is None or parent.is_running()) and current(job)),
|
|
445
|
+
event_sink=RecoveryJournal(args.config.parent.parent / "logs/qmt_strategy_recovery"))
|
|
446
|
+
states = {}
|
|
447
|
+
if args.report and args.report.exists():
|
|
448
|
+
states = json.loads(args.report.read_text(encoding="utf-8")).get("targets", {})
|
|
449
|
+
while True:
|
|
450
|
+
if parent is not None and not parent.is_running():
|
|
451
|
+
return 0
|
|
452
|
+
output = {"account_id": args.account_id, "checked_at": time.time(), "targets": {}}
|
|
453
|
+
try:
|
|
454
|
+
for key, original in jobs().items():
|
|
455
|
+
if original.get("account_id") != args.account_id:
|
|
456
|
+
continue
|
|
457
|
+
job = copy.deepcopy(original)
|
|
458
|
+
if key in states:
|
|
459
|
+
job["recovery"] = states[key]
|
|
460
|
+
result = recovery(job, inspect_only=not (args.ensure or args.watch))
|
|
461
|
+
output["targets"][key] = result
|
|
462
|
+
except Exception as error:
|
|
463
|
+
if not args.watch:
|
|
464
|
+
raise
|
|
465
|
+
output.update(targets=states, error=str(error))
|
|
466
|
+
states = output["targets"]
|
|
467
|
+
text = json.dumps(output, ensure_ascii=False, indent=2)
|
|
468
|
+
if args.report:
|
|
469
|
+
args.report.parent.mkdir(parents=True, exist_ok=True)
|
|
470
|
+
temporary = args.report.with_suffix(".pending")
|
|
471
|
+
temporary.write_text(text, encoding="utf-8")
|
|
472
|
+
temporary.replace(args.report)
|
|
473
|
+
if not args.watch:
|
|
474
|
+
print(text)
|
|
475
|
+
return 0 if states and all(s["state"] in ("running", "start_requested") for s in states.values()) else 1
|
|
476
|
+
time.sleep(1)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
if __name__ == "__main__":
|
|
480
|
+
sys.exit(main())
|