cfquant 0.2.59__tar.gz → 0.2.60__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.59 → cfquant-0.2.60}/PKG-INFO +2 -2
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/_editable_install.py +30 -4
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/normal_bridge.py +1 -1
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/protocol.py +2 -1
- cfquant-0.2.60/cfquant/qmt_binding_defaults.py +54 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/qmt_component_login.py +14 -2
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/qmt_dependencies.py +35 -3
- cfquant-0.2.60/cfquant/qmt_native_worker.py +253 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/qmt_strategy_recovery.py +7 -2
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/rpc_timing.py +2 -0
- cfquant-0.2.60/cfquant/tests/test_account_subscription_probe.py +233 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_lite_account_routing.py +4 -2
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_qmt_auto_login_browser.py +31 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_qmt_auto_login_integration.py +30 -6
- cfquant-0.2.60/cfquant/tests/test_qmt_binding_defaults.py +116 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_qmt_component_login.py +6 -6
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_qmt_dependencies.py +68 -0
- cfquant-0.2.60/cfquant/tests/test_qmt_native_worker.py +166 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_qmt_strategy_browser.py +1 -1
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_qmt_strategy_recovery.py +20 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_qmt_strategy_web.py +3 -3
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_subscription_congestion.py +7 -6
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_trader_subscription_recovery.py +21 -6
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tx_trade_bridge.py +33 -2
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/version.py +2 -2
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/xttrader.py +43 -7
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant.egg-info/PKG-INFO +2 -2
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant.egg-info/SOURCES.txt +5 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant.egg-info/requires.txt +1 -1
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant_web_server.py +92 -14
- {cfquant-0.2.59 → cfquant-0.2.60}/pyproject.toml +2 -2
- {cfquant-0.2.59 → cfquant-0.2.60}/qmt_scripts/CFQUANT_LITE.py +37 -5
- {cfquant-0.2.59 → cfquant-0.2.60}/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 +37 -5
- {cfquant-0.2.59 → cfquant-0.2.60}/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 +37 -5
- {cfquant-0.2.59 → cfquant-0.2.60}/requirements.txt +1 -1
- {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/app.js +5 -5
- {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/index.html +8 -8
- {cfquant-0.2.59 → cfquant-0.2.60}/LICENSE +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/LTtx/__init__.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/LTtx/tx/Config.txt +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/LTtx/tx/LTtx_server.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/LTtx/tx/__init__.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/LTtx/tx/tx.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/MANIFEST.in +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/README.md +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/__init__.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/account_routing.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/algo.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/algo_broker.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/batch_orders.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/build_info.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/callback_dispatcher.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/cftrader.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/channels.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/cli.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/client.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/config.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/diagnostics.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/flow_control.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/hub.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/level2.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/log_management.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/logging_i18n.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/management.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/order_identity.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/order_meta.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/pipe_bridge.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/pipe_client.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/pipe_hub.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/pipe_transport.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/qmt_binding_settings.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/qmt_bridge.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/qmt_component_agent.js +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/qmt_credentials.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/qmt_pipe_runtime.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/qmt_strategy_agent.js +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/qmt_strategy_deploy.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/qmt_strategy_package.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/qmt_strategy_runtime.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/qmt_update.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/quote_history.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/quote_leases.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/quote_service.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/remote_adapter.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/remote_management.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/remote_supervisor.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/runtime_report.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/shared_quotes.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/stock_connect.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/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.59 → cfquant-0.2.60}/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.59 → cfquant-0.2.60}/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.59 → cfquant-0.2.60}/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.59 → cfquant-0.2.60}/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.59 → cfquant-0.2.60}/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.59 → cfquant-0.2.60}/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.59 → cfquant-0.2.60}/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.59 → cfquant-0.2.60}/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.59 → cfquant-0.2.60}/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.59 → cfquant-0.2.60}/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.59 → cfquant-0.2.60}/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.59 → cfquant-0.2.60}/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.59 → cfquant-0.2.60}/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.59 → cfquant-0.2.60}/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.59 → cfquant-0.2.60}/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.59 → cfquant-0.2.60}/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.59 → cfquant-0.2.60}/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.59 → cfquant-0.2.60}/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.59 → cfquant-0.2.60}/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.59 → cfquant-0.2.60}/cfquant/tests/29_Level2/350/241/214/346/203/205/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/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.59 → cfquant-0.2.60}/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.59 → cfquant-0.2.60}/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.59 → cfquant-0.2.60}/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.59 → cfquant-0.2.60}/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.59 → cfquant-0.2.60}/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.59 → cfquant-0.2.60}/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.59 → cfquant-0.2.60}/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.59 → cfquant-0.2.60}/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.59 → cfquant-0.2.60}/cfquant/tests/6_/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/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.59 → cfquant-0.2.60}/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.59 → cfquant-0.2.60}/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.59 → cfquant-0.2.60}/cfquant/tests/_helpers.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/readme.md +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_account_query_timing.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_advanced_parity.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_algo.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_algo_broker.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_api_tester_browser.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_api_tester_web.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_binding_live_refresh_browser.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_binding_runtime_browser.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_callback_recovery.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_callback_requests.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_cftrader.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_cftrader_tutorial.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_cftrader_web.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_credit_compat.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_dataframe_encoding.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_json_serialization.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_latency_flow_control.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_lite_deployment_paths.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_lite_parity.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_lite_qmt_dispatch.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_local_trading_calendar.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_log_management.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_management.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_management_batch.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_market_data_fallback.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_market_timing.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_order_identity.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_order_time_callback.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_project_updater.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_python_api_reference.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_qmt_binding_settings.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_qmt_fileless.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_qmt_strategy_deploy.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_qmt_update.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_quote_latency.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_quote_latency_encoding.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_quote_service.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_remote_management.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_remote_management_browser.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_remote_version_check.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_shared_quotes.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_shared_quotes_generated.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_startup_recovery.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_stock_connect.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_stock_connect_browser.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_table_conversion.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_trade_strategy_callback.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_trading_dates.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_trading_monitor.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_trading_monitor_browser.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_tutorial_reader.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_tx_auto_import.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_tx_shutdown.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/trading_calendar.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/trading_monitor.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tx.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/xtconstant.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/xtdata.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/xttype.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant.egg-info/dependency_links.txt +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant.egg-info/entry_points.txt +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant.egg-info/top_level.txt +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/cfquant_pipe_hub.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/qmt_scripts/CFQUANT.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/qmt_scripts/__init__.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/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.59 → cfquant-0.2.60}/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.59 → cfquant-0.2.60}/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.59 → cfquant-0.2.60}/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.59 → cfquant-0.2.60}/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.59 → cfquant-0.2.60}/setup.cfg +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/__init__.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/api-tester.js +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/avatars/amber-pulse.svg +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/avatars/copper-grid.svg +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/avatars/market-blue.svg +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/avatars/rose-circuit.svg +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/avatars/signal-green.svg +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/avatars/slate-wave.svg +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/avatars/teal-orbit.svg +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/avatars/violet-node.svg +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/cftrader-api-data.js +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/docs.js +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/favicon.svg +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/python-api-data.js +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/python-api.js +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/remote-management.js +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/styles.css +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/trading-monitor.js +0 -0
- {cfquant-0.2.59 → cfquant-0.2.60}/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.60
|
|
4
4
|
Summary: QMT local bridge and xtquant-compatible Python client.
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.8
|
|
@@ -15,7 +15,7 @@ Requires-Dist: pytz
|
|
|
15
15
|
Requires-Dist: tabulate
|
|
16
16
|
Requires-Dist: pandas-market-calendars
|
|
17
17
|
Requires-Dist: importlib-resources>=5
|
|
18
|
-
Requires-Dist: frida
|
|
18
|
+
Requires-Dist: frida==17.16.4; sys_platform == "win32"
|
|
19
19
|
Requires-Dist: pywin32>=306; sys_platform == "win32"
|
|
20
20
|
Provides-Extra: dev
|
|
21
21
|
Requires-Dist: build>=1.0; extra == "dev"
|
|
@@ -10,9 +10,9 @@ import tempfile
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
|
|
12
12
|
if __package__:
|
|
13
|
-
from .qmt_dependencies import report_startup_dependencies
|
|
13
|
+
from .qmt_dependencies import FRIDA_REQUIREMENT, FRIDA_VERSION, report_startup_dependencies
|
|
14
14
|
else:
|
|
15
|
-
from qmt_dependencies import report_startup_dependencies
|
|
15
|
+
from qmt_dependencies import FRIDA_REQUIREMENT, FRIDA_VERSION, report_startup_dependencies
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
PACKAGE_NAME = "cfquant"
|
|
@@ -517,6 +517,32 @@ def installed_package_version(python_exe=None):
|
|
|
517
517
|
return completed.stdout.strip() if completed.returncode == 0 else ""
|
|
518
518
|
|
|
519
519
|
|
|
520
|
+
def reconcile_startup_frida(project_root, python_exe):
|
|
521
|
+
"""Repair version drift even when cfquant's own version did not change.
|
|
522
|
+
|
|
523
|
+
This launcher runs before Web/Frida loads. Failed optional repair must not
|
|
524
|
+
prevent manual QMT use; the native worker also checks before importing.
|
|
525
|
+
"""
|
|
526
|
+
report = report_startup_dependencies(python_exe) or {}
|
|
527
|
+
if os.environ.get("CFQUANT_QMT_AUTOMATION", "1").lower() in ("0", "false", "off"):
|
|
528
|
+
return
|
|
529
|
+
if not report.get("supported") or not report.get("packages"):
|
|
530
|
+
return
|
|
531
|
+
if report["packages"].get("frida") == FRIDA_VERSION:
|
|
532
|
+
return
|
|
533
|
+
print("[INFO] Installing QMT compatibility dependency: %s" % FRIDA_REQUIREMENT)
|
|
534
|
+
command = [python_exe, "-m", "pip", "install", "--disable-pip-version-check", "--no-input",
|
|
535
|
+
"--timeout", "15", "--retries", "1"] + pip_index_args() + [FRIDA_REQUIREMENT]
|
|
536
|
+
try:
|
|
537
|
+
completed = subprocess.run(command, cwd=str(project_root), env=pip_environment(),
|
|
538
|
+
timeout=120, **_hidden_subprocess_kwargs())
|
|
539
|
+
if completed.returncode:
|
|
540
|
+
raise RuntimeError("pip exit code %s" % completed.returncode)
|
|
541
|
+
except (OSError, RuntimeError, subprocess.TimeoutExpired) as error:
|
|
542
|
+
print("[WARN] Frida compatibility repair failed: %s. Web can continue; use manual QMT login." % error)
|
|
543
|
+
report_startup_dependencies(python_exe)
|
|
544
|
+
|
|
545
|
+
|
|
520
546
|
def ensure_cfquant_installed(project_root, python_exe=None):
|
|
521
547
|
project_root = Path(project_root).resolve()
|
|
522
548
|
if not (project_root / "pyproject.toml").is_file():
|
|
@@ -546,7 +572,7 @@ def ensure_cfquant_installed(project_root, python_exe=None):
|
|
|
546
572
|
print("cfquant installed=%s project=%s" % (installed or "missing", expected))
|
|
547
573
|
if installed == expected:
|
|
548
574
|
print("Package versions match; skip installation.")
|
|
549
|
-
|
|
575
|
+
reconcile_startup_frida(project_root, python_exe)
|
|
550
576
|
return 0
|
|
551
577
|
|
|
552
578
|
command = editable_install_args(project_root, python_exe)
|
|
@@ -570,7 +596,7 @@ def ensure_cfquant_installed(project_root, python_exe=None):
|
|
|
570
596
|
print("[ERROR] Installed cfquant version does not match the project after installation.", file=sys.stderr)
|
|
571
597
|
return 1
|
|
572
598
|
print("%s package is installed." % PACKAGE_NAME)
|
|
573
|
-
|
|
599
|
+
reconcile_startup_frida(project_root, python_exe)
|
|
574
600
|
return 0
|
|
575
601
|
|
|
576
602
|
|
|
@@ -194,7 +194,7 @@ class NormalQmtBridge(TxTradeBridge):
|
|
|
194
194
|
if action == "cfquant.status":
|
|
195
195
|
self._send_response(msg, self._status())
|
|
196
196
|
return
|
|
197
|
-
if action
|
|
197
|
+
if action in ("cfquant.strategy.control", "cfquant.account_subscription_status"):
|
|
198
198
|
try:
|
|
199
199
|
self._send_response(msg, self._dispatch(action, msg.get("params") or {}, msg))
|
|
200
200
|
except Exception as error:
|
|
@@ -154,7 +154,8 @@ def is_readonly_request(action):
|
|
|
154
154
|
# Only known query families. Downloads, subscriptions and all writes retain
|
|
155
155
|
# their existing execution semantics, including on an ambiguous timeout.
|
|
156
156
|
return (str(action).startswith(("xtdata.get_", "xttrader.query_"))
|
|
157
|
-
or action in ("cfquant.ping", "cfquant.status", "cfquant.query_info"
|
|
157
|
+
or action in ("cfquant.ping", "cfquant.status", "cfquant.query_info",
|
|
158
|
+
"cfquant.account_subscription_status"))
|
|
158
159
|
|
|
159
160
|
|
|
160
161
|
def request_expired(msg, now=None):
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""One-time migration of QMT automation preferences, using saved version evidence."""
|
|
2
|
+
import copy
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
AUTOMATION_DEFAULTS_VERSION = "0.2.59"
|
|
7
|
+
MIGRATION_KEY = "qmt_automation_defaults_migration"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def is_pre_automation_defaults_version(value):
|
|
11
|
+
"""Schema numbers, missing values and unrecognized versions are not evidence."""
|
|
12
|
+
match = re.fullmatch(r"v?(\d+)\.(\d+)\.(\d+)(?:(a|b|rc)\d+)?(?:\.post\d+)?(?:\+[-\w.]+)?", str(value or "").strip())
|
|
13
|
+
if not match:
|
|
14
|
+
return False
|
|
15
|
+
version = tuple(int(match.group(i)) for i in (1, 2, 3))
|
|
16
|
+
return version < (0, 2, 59) or (version == (0, 2, 59) and bool(match.group(4)))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def migrate_qmt_automation_defaults(raw):
|
|
20
|
+
"""Return a copy only when a known old configuration needs migration.
|
|
21
|
+
|
|
22
|
+
The running/installed version and QMT's version do not date user preferences.
|
|
23
|
+
They must never be used as substitutes for the saved configuration version.
|
|
24
|
+
"""
|
|
25
|
+
if not isinstance(raw, dict) or raw.get(MIGRATION_KEY):
|
|
26
|
+
return raw, 0
|
|
27
|
+
source_version = raw.get("saved_core_version", raw.get("core_version"))
|
|
28
|
+
if not is_pre_automation_defaults_version(source_version):
|
|
29
|
+
return raw, 0
|
|
30
|
+
result = copy.deepcopy(raw)
|
|
31
|
+
changed = 0
|
|
32
|
+
for collection in ("account_configs", "account_pairs"):
|
|
33
|
+
rows = result.get(collection) or {}
|
|
34
|
+
rows = rows.values() if isinstance(rows, dict) else rows if isinstance(rows, list) else []
|
|
35
|
+
for row in rows:
|
|
36
|
+
if not isinstance(row, dict) or not row.get("account_id"):
|
|
37
|
+
continue
|
|
38
|
+
login = row.get("qmt_auto_login")
|
|
39
|
+
login = dict(login) if isinstance(login, dict) else {}
|
|
40
|
+
login.update(enabled=True, auto_update=True)
|
|
41
|
+
# Preserve custom schedules and explicit empty schedules. Only absent
|
|
42
|
+
# schedules receive the same default as a new binding.
|
|
43
|
+
if not any(key in login for key in ("restart_times", "restartTimes", "restart_time", "restartTime")):
|
|
44
|
+
login["restart_times"] = ["09:00"]
|
|
45
|
+
row["qmt_auto_login"] = login
|
|
46
|
+
if collection == "account_configs":
|
|
47
|
+
strategy = row.get("qmt_strategy")
|
|
48
|
+
strategy = dict(strategy) if isinstance(strategy, dict) else {}
|
|
49
|
+
strategy.update(enabled=True, autorun=True)
|
|
50
|
+
row["qmt_strategy"] = strategy
|
|
51
|
+
changed += 1
|
|
52
|
+
result[MIGRATION_KEY] = {"version": AUTOMATION_DEFAULTS_VERSION, "source_version": str(source_version),
|
|
53
|
+
"bindings": changed}
|
|
54
|
+
return result, changed
|
|
@@ -11,6 +11,8 @@ from pathlib import Path
|
|
|
11
11
|
import sys
|
|
12
12
|
import time
|
|
13
13
|
|
|
14
|
+
from .qmt_native_worker import IsolatedLoginBackend, QmtAutomationCancelled, verify_environment_isolated
|
|
15
|
+
|
|
14
16
|
|
|
15
17
|
class QmtNotReady(RuntimeError):
|
|
16
18
|
pass
|
|
@@ -22,6 +24,11 @@ class QmtProcessExited(RuntimeError):
|
|
|
22
24
|
|
|
23
25
|
def verify_environment(directory):
|
|
24
26
|
"""Validate dependencies and native ABI before stopping a running terminal."""
|
|
27
|
+
verify_environment_isolated(directory)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _verify_environment_native(directory):
|
|
31
|
+
"""Only invoked inside the native worker or the standalone login CLI."""
|
|
25
32
|
try:
|
|
26
33
|
from .qmt_dependencies import LOGIN_MODULES, import_login_module
|
|
27
34
|
except ImportError:
|
|
@@ -163,7 +170,7 @@ class QtLoginBackend:
|
|
|
163
170
|
self.pid = matches[0].pid
|
|
164
171
|
if expected_pid is not None and self.pid != expected_pid:
|
|
165
172
|
raise RuntimeError("QMT process changed; stopping this login attempt")
|
|
166
|
-
|
|
173
|
+
_verify_environment_native(directory)
|
|
167
174
|
self.mutex = win32event.CreateMutex(None, False, "Local\\CFQuantQmtComponentLogin_" + str(self.pid))
|
|
168
175
|
acquired = win32event.WaitForSingleObject(self.mutex, 0)
|
|
169
176
|
if acquired not in (0, 0x80):
|
|
@@ -367,8 +374,11 @@ def login_running_qmt(directory, login_account="", password_provider=lambda: "",
|
|
|
367
374
|
if should_cancel():
|
|
368
375
|
return {"outcome": "cancelled", "login_succeeded": False, "attempts": []}
|
|
369
376
|
try:
|
|
370
|
-
backend =
|
|
377
|
+
backend = IsolatedLoginBackend(Path(directory), timeout, expected_pid=expected_pid,
|
|
378
|
+
should_cancel=should_cancel)
|
|
371
379
|
break
|
|
380
|
+
except QmtAutomationCancelled:
|
|
381
|
+
return {"outcome": "cancelled", "login_succeeded": False, "attempts": []}
|
|
372
382
|
except QmtProcessExited:
|
|
373
383
|
return {"outcome": "process_exited", "login_succeeded": False, "attempts": []}
|
|
374
384
|
except QmtNotReady:
|
|
@@ -380,6 +390,8 @@ def login_running_qmt(directory, login_account="", password_provider=lambda: "",
|
|
|
380
390
|
backend.should_cancel = should_cancel
|
|
381
391
|
return auto_login(backend, password_provider, retry_not_ready=True, login_account=login_account,
|
|
382
392
|
should_cancel=should_cancel, on_wait=on_wait)
|
|
393
|
+
except QmtAutomationCancelled:
|
|
394
|
+
return {"outcome": "cancelled", "login_succeeded": False, "attempts": []}
|
|
383
395
|
except QmtProcessExited:
|
|
384
396
|
return {"outcome": "process_exited", "login_succeeded": False, "attempts": []}
|
|
385
397
|
finally:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Read-only checks and repair instructions for optional QMT login components."""
|
|
2
2
|
import copy
|
|
3
3
|
import importlib
|
|
4
|
+
import importlib.metadata
|
|
4
5
|
import json
|
|
5
6
|
import os
|
|
6
7
|
import subprocess
|
|
@@ -10,6 +11,10 @@ import time
|
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
LOGIN_MODULES = ("frida", "win32api", "win32crypt", "win32event", "win32gui", "win32process")
|
|
14
|
+
# Verified QMT baseline. 17.25 introduced a Windows mapper that can abort the
|
|
15
|
+
# host on older ntdll builds; do not widen this without Windows/QMT validation.
|
|
16
|
+
FRIDA_VERSION = "17.16.4"
|
|
17
|
+
FRIDA_REQUIREMENT = "frida==" + FRIDA_VERSION
|
|
13
18
|
_LOCK = threading.Lock()
|
|
14
19
|
_CACHE = {}
|
|
15
20
|
_PROBE_CODE = r'''
|
|
@@ -17,6 +22,8 @@ import importlib, importlib.metadata, json, os, pathlib, sys
|
|
|
17
22
|
checks = []
|
|
18
23
|
for name in MODULES:
|
|
19
24
|
try:
|
|
25
|
+
if name == 'frida' and importlib.metadata.version('frida') != FRIDA_VERSION:
|
|
26
|
+
raise RuntimeError('Unsupported Frida version; install frida==' + FRIDA_VERSION)
|
|
20
27
|
module = importlib.import_module(name)
|
|
21
28
|
checks.append(dict(name=name, ok=True, path=getattr(module, '__file__', '')))
|
|
22
29
|
except Exception as error:
|
|
@@ -46,6 +53,16 @@ def _ps_quote(value):
|
|
|
46
53
|
|
|
47
54
|
|
|
48
55
|
def _finish_report(report):
|
|
56
|
+
if report.get("supported"):
|
|
57
|
+
version = (report.get("packages") or {}).get("frida")
|
|
58
|
+
if version and version != FRIDA_VERSION:
|
|
59
|
+
checks = report.setdefault("checks", [])
|
|
60
|
+
check = next((item for item in checks if item["name"] == "frida"), None)
|
|
61
|
+
if check is None:
|
|
62
|
+
check = {"name": "frida"}
|
|
63
|
+
checks.append(check)
|
|
64
|
+
check.update(ok=False, error="Frida %s 未通过本项目兼容性验证,请安装 %s" % (version, FRIDA_REQUIREMENT))
|
|
65
|
+
report["frida_requirement"] = FRIDA_REQUIREMENT
|
|
49
66
|
report["checked_at"] = time.time()
|
|
50
67
|
report["ok"] = bool(report.get("supported") and not report.get("error")
|
|
51
68
|
and all(item["ok"] for item in report.get("checks", [])))
|
|
@@ -73,8 +90,10 @@ def _finish_report(report):
|
|
|
73
90
|
if environment == "python":
|
|
74
91
|
commands.append(python + " -m pywin32_postinstall -install")
|
|
75
92
|
if any(item["name"] == "frida" for item in failed):
|
|
76
|
-
commands.append(python + " -m pip install --no-cache-dir --
|
|
77
|
-
commands.append(python + " -c " + _ps_quote(
|
|
93
|
+
commands.append(python + " -m pip install --no-cache-dir --force-reinstall '" + FRIDA_REQUIREMENT + "'")
|
|
94
|
+
commands.append(python + " -c " + _ps_quote(
|
|
95
|
+
"from cfquant.qmt_dependencies import report_startup_dependencies; "
|
|
96
|
+
"r=report_startup_dependencies(); raise SystemExit(0 if r['ok'] else 1)"))
|
|
78
97
|
report["repair_commands"] = commands
|
|
79
98
|
if environment == "conda":
|
|
80
99
|
report["repair_note"] = (
|
|
@@ -111,7 +130,7 @@ def check_login_dependencies(python_exe=None, refresh=False, timeout=15):
|
|
|
111
130
|
if os.name == "nt":
|
|
112
131
|
run_kwargs["creationflags"] = getattr(subprocess, "CREATE_NO_WINDOW", 0)
|
|
113
132
|
completed = subprocess.run(
|
|
114
|
-
[executable, "-c", "MODULES = %r\n" % (LOGIN_MODULES,) + _PROBE_CODE],
|
|
133
|
+
[executable, "-c", "MODULES = %r\nFRIDA_VERSION = %r\n" % (LOGIN_MODULES, FRIDA_VERSION) + _PROBE_CODE],
|
|
115
134
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding="utf-8",
|
|
116
135
|
errors="replace", timeout=timeout,
|
|
117
136
|
**run_kwargs,
|
|
@@ -130,8 +149,21 @@ class QmtDependencyError(RuntimeError):
|
|
|
130
149
|
pass
|
|
131
150
|
|
|
132
151
|
|
|
152
|
+
def require_compatible_frida():
|
|
153
|
+
try:
|
|
154
|
+
version = importlib.metadata.version("frida")
|
|
155
|
+
except importlib.metadata.PackageNotFoundError:
|
|
156
|
+
version = "未安装"
|
|
157
|
+
if version != FRIDA_VERSION:
|
|
158
|
+
raise QmtDependencyError(
|
|
159
|
+
"Frida %s 未通过本项目兼容性验证,已跳过 QMT 自动操作。"
|
|
160
|
+
"请在当前 Python 环境安装 %s;手动登录及行情交易接口仍可使用。" % (version, FRIDA_REQUIREMENT))
|
|
161
|
+
|
|
162
|
+
|
|
133
163
|
def import_login_module(name):
|
|
134
164
|
"""Keep in-process import errors actionable, including stale loaded DLLs."""
|
|
165
|
+
if name == "frida":
|
|
166
|
+
require_compatible_frida()
|
|
135
167
|
try:
|
|
136
168
|
return importlib.import_module(name)
|
|
137
169
|
except (ImportError, OSError) as error:
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""Isolate QMT/Frida native code from the Web process using private stdio RPC.
|
|
2
|
+
|
|
3
|
+
Credentials only travel over the child's stdin, never argv, files or logs.
|
|
4
|
+
Requests are not replayed after a crash or timeout: a native action may have run.
|
|
5
|
+
"""
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import queue
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
LOGIN_METHODS = frozenset(("already_logged_in", "state", "select_account", "submit",
|
|
17
|
+
"fill", "wait_outcome", "acknowledge_failure"))
|
|
18
|
+
STRATEGY_METHODS = frozenset(("snapshot", "start_strategy", "notices", "dismiss_notice"))
|
|
19
|
+
MAX_MESSAGE = 4 * 1024 * 1024
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class QmtAutomationError(RuntimeError):
|
|
23
|
+
"""A failed native worker; safe to display without native exception contents."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class QmtAutomationCancelled(QmtAutomationError):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class NativeWorker:
|
|
31
|
+
def __init__(self, kind, directory, *, timeout=40, expected_pid=None,
|
|
32
|
+
should_cancel=lambda: False, command=None):
|
|
33
|
+
if os.environ.get("CFQUANT_QMT_AUTOMATION", "1").lower() in ("0", "false", "off"):
|
|
34
|
+
raise QmtAutomationError("QMT 原生自动化已关闭,请手动登录并启动策略;Web 服务及行情交易接口仍可使用。")
|
|
35
|
+
self.should_cancel = should_cancel
|
|
36
|
+
self.call_timeout = max(15.0, float(timeout) + 10)
|
|
37
|
+
self.methods = LOGIN_METHODS if kind == "login" else STRATEGY_METHODS
|
|
38
|
+
self._lock = threading.RLock()
|
|
39
|
+
self._responses = queue.Queue()
|
|
40
|
+
self._sequence = 0
|
|
41
|
+
self._closed = False
|
|
42
|
+
self._tls_error = False
|
|
43
|
+
env = dict(os.environ, PYTHONIOENCODING="utf-8", PYTHONDONTWRITEBYTECODE="1")
|
|
44
|
+
self.process = subprocess.Popen(
|
|
45
|
+
command or [sys.executable, "-u", "-m", "cfquant.qmt_native_worker"],
|
|
46
|
+
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
47
|
+
text=True, encoding="utf-8", errors="replace", bufsize=1, env=env,
|
|
48
|
+
cwd=str(Path(__file__).resolve().parents[1]),
|
|
49
|
+
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0) if os.name == "nt" else 0,
|
|
50
|
+
)
|
|
51
|
+
self._readers = [
|
|
52
|
+
threading.Thread(target=self._read_responses, name="qmt-native-response", daemon=True),
|
|
53
|
+
threading.Thread(target=self._read_errors, name="qmt-native-stderr", daemon=True),
|
|
54
|
+
]
|
|
55
|
+
for thread in self._readers:
|
|
56
|
+
thread.start()
|
|
57
|
+
try:
|
|
58
|
+
info = self.call("open", kind=kind, directory=str(directory), timeout=timeout,
|
|
59
|
+
expected_pid=expected_pid)
|
|
60
|
+
self.pid = info.get("pid")
|
|
61
|
+
self.main_hwnd = info.get("main_hwnd")
|
|
62
|
+
except BaseException:
|
|
63
|
+
self.close(force=True)
|
|
64
|
+
raise
|
|
65
|
+
|
|
66
|
+
def _read_responses(self):
|
|
67
|
+
try:
|
|
68
|
+
while True:
|
|
69
|
+
line = self.process.stdout.readline(MAX_MESSAGE + 1)
|
|
70
|
+
if not line or len(line) > MAX_MESSAGE:
|
|
71
|
+
break
|
|
72
|
+
self._responses.put(json.loads(line))
|
|
73
|
+
except (ValueError, OSError):
|
|
74
|
+
pass
|
|
75
|
+
finally:
|
|
76
|
+
self._responses.put(None)
|
|
77
|
+
|
|
78
|
+
def _read_errors(self):
|
|
79
|
+
try:
|
|
80
|
+
while True:
|
|
81
|
+
chunk = self.process.stderr.readline(4096)
|
|
82
|
+
if not chunk:
|
|
83
|
+
break
|
|
84
|
+
if "Unable to locate the ntdll loader TLS routines" in chunk:
|
|
85
|
+
self._tls_error = True
|
|
86
|
+
# Native output can contain input values; never forward it.
|
|
87
|
+
except (ValueError, OSError):
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
def _failure(self, reason):
|
|
91
|
+
from .qmt_dependencies import FRIDA_REQUIREMENT
|
|
92
|
+
detail = "Frida 与当前 Windows 不兼容" if self._tls_error else reason
|
|
93
|
+
return QmtAutomationError(
|
|
94
|
+
"QMT 自动化子进程%s,已停止本次自动操作,Web 服务继续运行。"
|
|
95
|
+
"请检查自动登录组件并安装 %s,或手动登录、启动 QMT 策略。" % (detail, FRIDA_REQUIREMENT))
|
|
96
|
+
|
|
97
|
+
def call(self, method, *args, **kwargs):
|
|
98
|
+
with self._lock:
|
|
99
|
+
if self._closed:
|
|
100
|
+
raise self._failure("已关闭")
|
|
101
|
+
self._sequence += 1
|
|
102
|
+
request_id = self._sequence
|
|
103
|
+
try:
|
|
104
|
+
if self.should_cancel():
|
|
105
|
+
raise QmtAutomationCancelled("QMT 自动操作已取消")
|
|
106
|
+
message = json.dumps(dict(id=request_id, method=method, args=args, kwargs=kwargs), ensure_ascii=True)
|
|
107
|
+
if len(message) > MAX_MESSAGE:
|
|
108
|
+
raise self._failure("请求过大")
|
|
109
|
+
self.process.stdin.write(message + "\n")
|
|
110
|
+
self.process.stdin.flush()
|
|
111
|
+
deadline = time.monotonic() + self.call_timeout
|
|
112
|
+
while True:
|
|
113
|
+
if self.should_cancel():
|
|
114
|
+
raise QmtAutomationCancelled("QMT 自动操作已取消")
|
|
115
|
+
remaining = deadline - time.monotonic()
|
|
116
|
+
if remaining <= 0:
|
|
117
|
+
raise self._failure("响应超时")
|
|
118
|
+
try:
|
|
119
|
+
reply = self._responses.get(timeout=min(.1, remaining))
|
|
120
|
+
break
|
|
121
|
+
except queue.Empty:
|
|
122
|
+
continue
|
|
123
|
+
if not isinstance(reply, dict) or reply.get("id") != request_id:
|
|
124
|
+
raise self._failure("异常退出或通信中断")
|
|
125
|
+
except (BrokenPipeError, OSError) as error:
|
|
126
|
+
self.close(force=True)
|
|
127
|
+
raise self._failure("通信中断") from error
|
|
128
|
+
except BaseException:
|
|
129
|
+
self.close(force=True)
|
|
130
|
+
raise
|
|
131
|
+
if not reply.get("ok"):
|
|
132
|
+
self._raise_remote(reply)
|
|
133
|
+
return reply.get("result")
|
|
134
|
+
|
|
135
|
+
@staticmethod
|
|
136
|
+
def _raise_remote(reply):
|
|
137
|
+
from .qmt_component_login import QmtNotReady, QmtProcessExited
|
|
138
|
+
from .qmt_dependencies import QmtDependencyError
|
|
139
|
+
from .qmt_strategy_recovery import UnsupportedQmt
|
|
140
|
+
errors = {cls.__name__: cls for cls in (QmtNotReady, QmtProcessExited, QmtDependencyError, UnsupportedQmt)}
|
|
141
|
+
raise errors.get(reply.get("error_type"), RuntimeError)(reply.get("error") or "QMT native operation failed")
|
|
142
|
+
|
|
143
|
+
def __getattr__(self, name):
|
|
144
|
+
if name not in self.methods:
|
|
145
|
+
raise AttributeError(name)
|
|
146
|
+
return lambda *args, **kwargs: self.call(name, *args, **kwargs)
|
|
147
|
+
|
|
148
|
+
def close(self, force=False):
|
|
149
|
+
with self._lock:
|
|
150
|
+
if self._closed:
|
|
151
|
+
return
|
|
152
|
+
self._closed = True
|
|
153
|
+
try:
|
|
154
|
+
if not force and self.process.poll() is None:
|
|
155
|
+
self.process.stdin.write(json.dumps(dict(id=0, method="close")) + "\n")
|
|
156
|
+
self.process.stdin.flush()
|
|
157
|
+
self.process.wait(timeout=1)
|
|
158
|
+
except (OSError, ValueError, subprocess.TimeoutExpired):
|
|
159
|
+
pass
|
|
160
|
+
finally:
|
|
161
|
+
if self.process.poll() is None:
|
|
162
|
+
self.process.kill()
|
|
163
|
+
self.process.wait(timeout=5)
|
|
164
|
+
try:
|
|
165
|
+
self.process.stdin.close()
|
|
166
|
+
except OSError:
|
|
167
|
+
pass # A crash may leave buffered input with no reader.
|
|
168
|
+
for thread in self._readers:
|
|
169
|
+
thread.join(timeout=1)
|
|
170
|
+
self.process.stdout.close()
|
|
171
|
+
self.process.stderr.close()
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class IsolatedLoginBackend(NativeWorker):
|
|
175
|
+
def __init__(self, directory, timeout=40, expected_pid=None, *, should_cancel=lambda: False):
|
|
176
|
+
super().__init__("login", directory, timeout=timeout, expected_pid=expected_pid, should_cancel=should_cancel)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class IsolatedStrategyBackend(NativeWorker):
|
|
180
|
+
def __init__(self, directory):
|
|
181
|
+
super().__init__("strategy", directory, timeout=15)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def verify_environment_isolated(directory):
|
|
185
|
+
worker = NativeWorker("verify", directory, timeout=5)
|
|
186
|
+
worker.close()
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _open_backend(kind, directory, timeout=40, expected_pid=None):
|
|
190
|
+
from .qmt_component_login import QtLoginBackend, _verify_environment_native
|
|
191
|
+
if kind == "verify":
|
|
192
|
+
_verify_environment_native(directory)
|
|
193
|
+
return None
|
|
194
|
+
if kind == "login":
|
|
195
|
+
return QtLoginBackend(Path(directory), timeout, expected_pid=expected_pid)
|
|
196
|
+
if kind == "strategy":
|
|
197
|
+
from .qmt_strategy_recovery import QtStrategyBackend
|
|
198
|
+
return QtStrategyBackend(directory)
|
|
199
|
+
raise ValueError("Unknown QMT worker kind")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def serve(factory=_open_backend):
|
|
203
|
+
# Reserve stdout for protocol, including when a native DLL prints to fd 1.
|
|
204
|
+
output = os.fdopen(os.dup(sys.stdout.fileno()), "w", encoding="utf-8", buffering=1)
|
|
205
|
+
os.dup2(sys.stderr.fileno(), sys.stdout.fileno())
|
|
206
|
+
sys.stdout = sys.stderr
|
|
207
|
+
requests = queue.Queue(maxsize=2)
|
|
208
|
+
def receive():
|
|
209
|
+
try:
|
|
210
|
+
while True:
|
|
211
|
+
line = sys.stdin.readline(MAX_MESSAGE + 1)
|
|
212
|
+
if not line or len(line) > MAX_MESSAGE:
|
|
213
|
+
break
|
|
214
|
+
requests.put(json.loads(line))
|
|
215
|
+
finally:
|
|
216
|
+
# Also stop a hung native call if the Web parent exits unexpectedly.
|
|
217
|
+
os._exit(0)
|
|
218
|
+
threading.Thread(target=receive, name="qmt-native-input", daemon=True).start()
|
|
219
|
+
backend = None
|
|
220
|
+
opened = False
|
|
221
|
+
methods = frozenset()
|
|
222
|
+
try:
|
|
223
|
+
while True:
|
|
224
|
+
request = requests.get()
|
|
225
|
+
method = request.get("method")
|
|
226
|
+
if method == "close":
|
|
227
|
+
break
|
|
228
|
+
try:
|
|
229
|
+
if method == "open" and not opened:
|
|
230
|
+
kwargs = request["kwargs"]
|
|
231
|
+
backend = factory(**kwargs)
|
|
232
|
+
opened = True
|
|
233
|
+
methods = LOGIN_METHODS if kwargs["kind"] == "login" else STRATEGY_METHODS
|
|
234
|
+
result = {key: getattr(backend, key, None) for key in ("pid", "main_hwnd")}
|
|
235
|
+
elif opened and backend is not None and method in methods:
|
|
236
|
+
result = getattr(backend, method)(*request.get("args", []), **request.get("kwargs", {}))
|
|
237
|
+
else:
|
|
238
|
+
raise ValueError("Unsupported QMT worker request")
|
|
239
|
+
reply = dict(id=request["id"], ok=True, result=result)
|
|
240
|
+
except Exception as error:
|
|
241
|
+
reply = dict(id=request["id"], ok=False, error_type=type(error).__name__,
|
|
242
|
+
error="QMT credential entry failed" if method == "fill" else str(error))
|
|
243
|
+
output.write(json.dumps(reply, ensure_ascii=True) + "\n")
|
|
244
|
+
output.flush()
|
|
245
|
+
request = None
|
|
246
|
+
finally:
|
|
247
|
+
if backend is not None:
|
|
248
|
+
backend.close()
|
|
249
|
+
output.close()
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
if __name__ == "__main__":
|
|
253
|
+
serve()
|
|
@@ -16,6 +16,8 @@ import time
|
|
|
16
16
|
import uuid
|
|
17
17
|
|
|
18
18
|
from .qmt_component_login import QtLoginBackend, QmtNotReady
|
|
19
|
+
from .qmt_native_worker import IsolatedStrategyBackend, QmtAutomationError
|
|
20
|
+
from .qmt_dependencies import QmtDependencyError
|
|
19
21
|
from .qmt_strategy_deploy import (
|
|
20
22
|
_role_stock, _strategy_slot_name, account_qmt_roots,
|
|
21
23
|
_write_json, _digest, PipeStrategyController, normalize_strategy_mode, normalize_strategy_settings, qmt_root,
|
|
@@ -166,7 +168,7 @@ def match_models(snapshot, expected):
|
|
|
166
168
|
|
|
167
169
|
|
|
168
170
|
class QmtStrategyRecovery:
|
|
169
|
-
def __init__(self, accounts_provider, *, backend_factory=
|
|
171
|
+
def __init__(self, accounts_provider, *, backend_factory=IsolatedStrategyBackend,
|
|
170
172
|
job_is_current=lambda job: True, clock=time.time, interval=20.0,
|
|
171
173
|
notice_probe=visible_notice_windows, event_sink=None, bridge_verifier=confirm_recovery_bridge):
|
|
172
174
|
self.accounts_provider = accounts_provider
|
|
@@ -235,10 +237,12 @@ class QmtStrategyRecovery:
|
|
|
235
237
|
return job["recovery"]
|
|
236
238
|
previous = job.get("recovery") or {}
|
|
237
239
|
if (not inspect_only and now < previous.get("next_check_at", 0)
|
|
238
|
-
and
|
|
240
|
+
and (previous.get("native_failure")
|
|
241
|
+
or not self.notice_probe(previous.get("pid"), previous.get("main_hwnd")))):
|
|
239
242
|
return previous
|
|
240
243
|
status = dict(previous, last_checked_at=now, next_check_at=now + self.interval)
|
|
241
244
|
status.pop("error", None)
|
|
245
|
+
status.pop("native_failure", None)
|
|
242
246
|
backend = None
|
|
243
247
|
try:
|
|
244
248
|
backend = self.backend_factory(job["root"])
|
|
@@ -303,6 +307,7 @@ class QmtStrategyRecovery:
|
|
|
303
307
|
except Exception as error:
|
|
304
308
|
status.update(state="unsupported" if isinstance(error, UnsupportedQmt) else "unavailable",
|
|
305
309
|
error=str(error), next_check_at=now + max(self.interval, 60))
|
|
310
|
+
status["native_failure"] = isinstance(error, (QmtAutomationError, QmtDependencyError))
|
|
306
311
|
if status.get("incident") and status.get("last_logged_error") != str(error):
|
|
307
312
|
try:
|
|
308
313
|
self._event(job, status, "illegal_ip_processing_failed", error=str(error))
|
|
@@ -38,6 +38,8 @@ class RpcTiming(object):
|
|
|
38
38
|
if self.sent_at is not None:
|
|
39
39
|
self.output["rpc"]["response_wait_ms"] = round((self.received_at - self.sent_at) * 1000, 3)
|
|
40
40
|
meta = message.get("meta") or {}
|
|
41
|
+
if "web_queue_ms" in meta:
|
|
42
|
+
self.output["web"] = {"queue_ms": meta["web_queue_ms"], "route": meta.get("route")}
|
|
41
43
|
qmt = (meta.get("timing") or {}).get("qmt")
|
|
42
44
|
self.output["qmt"] = dict(qmt) if isinstance(qmt, dict) else {
|
|
43
45
|
"available": False, "reason": "bridge_timing_unavailable",
|