cfquant 0.2.53__tar.gz → 0.2.55__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.53 → cfquant-0.2.55}/MANIFEST.in +1 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/PKG-INFO +3 -1
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/_editable_install.py +7 -0
- cfquant-0.2.55/cfquant/qmt_component_agent.js +249 -0
- cfquant-0.2.55/cfquant/qmt_component_login.py +321 -0
- cfquant-0.2.55/cfquant/qmt_credentials.py +78 -0
- cfquant-0.2.55/cfquant/qmt_dependencies.py +137 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/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 +1 -1
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_lite_deployment_paths.py +1 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_management.py +7 -2
- cfquant-0.2.55/cfquant/tests/test_qmt_auto_login_browser.py +199 -0
- cfquant-0.2.55/cfquant/tests/test_qmt_auto_login_integration.py +356 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_qmt_binding_settings.py +2 -2
- cfquant-0.2.55/cfquant/tests/test_qmt_component_login.py +165 -0
- cfquant-0.2.55/cfquant/tests/test_qmt_dependencies.py +55 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_qmt_strategy_browser.py +15 -6
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_qmt_strategy_web.py +44 -34
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_setup_qmt_prompt.py +1 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_stock_connect.py +1 -1
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/trading_calendar.py +8 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/version.py +2 -2
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant.egg-info/PKG-INFO +3 -1
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant.egg-info/SOURCES.txt +8 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant.egg-info/requires.txt +4 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant_web_server.py +303 -116
- {cfquant-0.2.53 → cfquant-0.2.55}/pyproject.toml +4 -1
- {cfquant-0.2.53 → cfquant-0.2.55}/qmt_scripts/CFQUANT_LITE.py +1 -1
- {cfquant-0.2.53 → cfquant-0.2.55}/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.53 → cfquant-0.2.55}/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.53 → cfquant-0.2.55}/requirements.txt +2 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/app.js +323 -15
- {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/index.html +53 -35
- {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/styles.css +42 -1
- {cfquant-0.2.53 → cfquant-0.2.55}/LICENSE +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/LTtx/__init__.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/LTtx/tx/Config.txt +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/LTtx/tx/LTtx_server.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/LTtx/tx/__init__.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/LTtx/tx/tx.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/README.md +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/__init__.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/account_routing.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/batch_orders.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/build_info.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/callback_dispatcher.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/cftrader.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/channels.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/cli.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/client.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/config.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/diagnostics.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/flow_control.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/hub.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/level2.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/log_management.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/logging_i18n.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/management.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/normal_bridge.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/order_identity.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/order_meta.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/pipe_bridge.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/pipe_client.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/pipe_hub.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/pipe_transport.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/protocol.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/qmt_binding_settings.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/qmt_bridge.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/qmt_pipe_runtime.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/qmt_strategy_deploy.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/qmt_strategy_package.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/qmt_strategy_runtime.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/quote_history.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/rpc_timing.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/runtime_report.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/stock_connect.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/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.53 → cfquant-0.2.55}/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.53 → cfquant-0.2.55}/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.53 → cfquant-0.2.55}/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.53 → cfquant-0.2.55}/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.53 → cfquant-0.2.55}/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.53 → cfquant-0.2.55}/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.53 → cfquant-0.2.55}/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.53 → cfquant-0.2.55}/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.53 → cfquant-0.2.55}/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.53 → cfquant-0.2.55}/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.53 → cfquant-0.2.55}/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.53 → cfquant-0.2.55}/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.53 → cfquant-0.2.55}/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.53 → cfquant-0.2.55}/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.53 → cfquant-0.2.55}/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.53 → cfquant-0.2.55}/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.53 → cfquant-0.2.55}/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.53 → cfquant-0.2.55}/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.53 → cfquant-0.2.55}/cfquant/tests/29_Level2/350/241/214/346/203/205/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/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.53 → cfquant-0.2.55}/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.53 → cfquant-0.2.55}/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.53 → cfquant-0.2.55}/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.53 → cfquant-0.2.55}/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.53 → cfquant-0.2.55}/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.53 → cfquant-0.2.55}/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.53 → cfquant-0.2.55}/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.53 → cfquant-0.2.55}/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.53 → cfquant-0.2.55}/cfquant/tests/6_/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/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.53 → cfquant-0.2.55}/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.53 → cfquant-0.2.55}/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.53 → cfquant-0.2.55}/cfquant/tests/_helpers.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/log/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_20260911_024617.md" +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/log/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_20260921_175732.md" +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/readme.md +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_advanced_parity.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_api_tester_browser.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_api_tester_web.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_binding_runtime_browser.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_callback_requests.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_cftrader.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_cftrader_tutorial.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_cftrader_web.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_credit_compat.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_dataframe_encoding.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_json_serialization.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_latency_flow_control.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_lite_account_routing.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_lite_parity.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_lite_qmt_dispatch.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_local_trading_calendar.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_log_management.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_management_batch.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_market_data_fallback.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_market_timing.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_order_identity.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_order_time_callback.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_project_updater.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_python_api_reference.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_qmt_fileless.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_qmt_strategy_deploy.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_quote_latency.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_quote_latency_encoding.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_startup_recovery.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_stock_connect_browser.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_table_conversion.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_trade_strategy_callback.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_trading_dates.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_tutorial_reader.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_tx_auto_import.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_tx_shutdown.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tx.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tx_trade_bridge.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/xtconstant.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/xtdata.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/xttrader.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/xttype.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant.egg-info/dependency_links.txt +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant.egg-info/entry_points.txt +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant.egg-info/top_level.txt +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/cfquant_pipe_hub.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/qmt_scripts/CFQUANT.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/qmt_scripts/__init__.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/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.53 → cfquant-0.2.55}/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.53 → cfquant-0.2.55}/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.53 → cfquant-0.2.55}/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.53 → cfquant-0.2.55}/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.53 → cfquant-0.2.55}/setup.cfg +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/__init__.py +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/api-tester.js +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/avatars/amber-pulse.svg +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/avatars/copper-grid.svg +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/avatars/market-blue.svg +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/avatars/rose-circuit.svg +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/avatars/signal-green.svg +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/avatars/slate-wave.svg +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/avatars/teal-orbit.svg +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/avatars/violet-node.svg +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/cftrader-api-data.js +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/docs.js +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/favicon.svg +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/python-api-data.js +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/python-api.js +0 -0
- {cfquant-0.2.53 → cfquant-0.2.55}/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.55
|
|
4
4
|
Summary: QMT local bridge and xtquant-compatible Python client.
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.8
|
|
@@ -14,6 +14,8 @@ Requires-Dist: pytz
|
|
|
14
14
|
Requires-Dist: tabulate
|
|
15
15
|
Requires-Dist: pandas-market-calendars
|
|
16
16
|
Requires-Dist: importlib-resources>=5
|
|
17
|
+
Requires-Dist: frida<18,>=17.0; sys_platform == "win32"
|
|
18
|
+
Requires-Dist: pywin32>=306; sys_platform == "win32"
|
|
17
19
|
Provides-Extra: dev
|
|
18
20
|
Requires-Dist: build>=1.0; extra == "dev"
|
|
19
21
|
Requires-Dist: twine>=5.0; extra == "dev"
|
|
@@ -9,6 +9,11 @@ import sys
|
|
|
9
9
|
import tempfile
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
|
|
12
|
+
if __package__:
|
|
13
|
+
from .qmt_dependencies import report_startup_dependencies
|
|
14
|
+
else:
|
|
15
|
+
from qmt_dependencies import report_startup_dependencies
|
|
16
|
+
|
|
12
17
|
|
|
13
18
|
PACKAGE_NAME = "cfquant"
|
|
14
19
|
|
|
@@ -503,6 +508,7 @@ def ensure_cfquant_installed(project_root, python_exe=None):
|
|
|
503
508
|
print("cfquant installed=%s project=%s" % (installed or "missing", expected))
|
|
504
509
|
if installed == expected:
|
|
505
510
|
print("Package versions match; skip installation.")
|
|
511
|
+
report_startup_dependencies(python_exe)
|
|
506
512
|
return 0
|
|
507
513
|
|
|
508
514
|
command = editable_install_args(project_root, python_exe)
|
|
@@ -526,6 +532,7 @@ def ensure_cfquant_installed(project_root, python_exe=None):
|
|
|
526
532
|
print("[ERROR] Installed cfquant version does not match the project after installation.", file=sys.stderr)
|
|
527
533
|
return 1
|
|
528
534
|
print("%s package is installed." % PACKAGE_NAME)
|
|
535
|
+
report_startup_dependencies(python_exe)
|
|
529
536
|
return 0
|
|
530
537
|
|
|
531
538
|
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const core = Process.getModuleByName('Qt5Core.dll');
|
|
4
|
+
const widgets = Process.getModuleByName('Qt5Widgets.dll');
|
|
5
|
+
const user32 = Process.getModuleByName('user32.dll');
|
|
6
|
+
const fn = (mod, name, ret, args) => new NativeFunction(mod.getExportByName(name), ret, args);
|
|
7
|
+
const findWidget = fn(widgets, '?find@QWidget@@SAPEAV1@_K@Z', 'pointer', ['uint64']);
|
|
8
|
+
const children = fn(core, '?children@QObject@@QEBAAEBV?$QList@PEAVQObject@@@@XZ', 'pointer', ['pointer']);
|
|
9
|
+
const inherits = fn(core, '?inherits@QObject@@QEBA_NPEBD@Z', 'bool', ['pointer', 'pointer']);
|
|
10
|
+
const className = fn(core, '?className@QMetaObject@@QEBAPEBDXZ', 'pointer', ['pointer']);
|
|
11
|
+
const isVisible = fn(widgets, '?isVisible@QWidget@@QEBA_NXZ', 'bool', ['pointer']);
|
|
12
|
+
const isEnabled = fn(widgets, '?isEnabled@QWidget@@QEBA_NXZ', 'bool', ['pointer']);
|
|
13
|
+
const echoMode = fn(widgets, '?echoMode@QLineEdit@@QEBA?AW4EchoMode@1@XZ', 'int', ['pointer']);
|
|
14
|
+
const setText = fn(widgets, '?setText@QLineEdit@@QEAAXAEBVQString@@@Z', 'void', ['pointer', 'pointer']);
|
|
15
|
+
const stringCtor = fn(core, '??0QString@@QEAA@PEBVQChar@@H@Z', 'pointer', ['pointer', 'pointer', 'int']);
|
|
16
|
+
const stringDtor = fn(core, '??1QString@@QEAA@XZ', 'void', ['pointer']);
|
|
17
|
+
const stringSize = fn(core, '?size@QString@@QEBAHXZ', 'int', ['pointer']);
|
|
18
|
+
const stringData = fn(core, '?utf16@QString@@QEBAPEBGXZ', 'pointer', ['pointer']);
|
|
19
|
+
const buttonText = fn(widgets, '?text@QAbstractButton@@QEBA?AVQString@@XZ', 'pointer', ['pointer', 'pointer']);
|
|
20
|
+
const editText = fn(widgets, '?text@QLineEdit@@QEBA?AVQString@@XZ', 'pointer', ['pointer', 'pointer']);
|
|
21
|
+
const labelText = fn(widgets, '?text@QLabel@@QEBA?AVQString@@XZ', 'pointer', ['pointer', 'pointer']);
|
|
22
|
+
const buttonClick = fn(widgets, '?click@QAbstractButton@@QEAAXXZ', 'void', ['pointer']);
|
|
23
|
+
const closeWidget = fn(widgets, '?close@QWidget@@QEAA_NXZ', 'bool', ['pointer']);
|
|
24
|
+
const comboEditable = fn(widgets, '?isEditable@QComboBox@@QEBA_NXZ', 'bool', ['pointer']);
|
|
25
|
+
const comboSetText = fn(widgets, '?setEditText@QComboBox@@QEAAXAEBVQString@@@Z', 'void', ['pointer', 'pointer']);
|
|
26
|
+
const getThread = fn(user32, 'GetWindowThreadProcessId', 'uint32', ['pointer', 'pointer']);
|
|
27
|
+
const postMessage = fn(user32, 'PostMessageW', 'bool', ['pointer', 'uint32', 'pointer', 'pointer']);
|
|
28
|
+
const typeNames = new Map();
|
|
29
|
+
let pending = null;
|
|
30
|
+
|
|
31
|
+
function hasType(object, name) {
|
|
32
|
+
if (!typeNames.has(name)) typeNames.set(name, Memory.allocUtf8String(name));
|
|
33
|
+
return inherits(object, typeNames.get(name));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function typeName(object) {
|
|
37
|
+
const metaFn = new NativeFunction(object.readPointer().readPointer(), 'pointer', ['pointer']);
|
|
38
|
+
return className(metaFn(object)).readUtf8String();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function childObjects(object) {
|
|
42
|
+
const d = children(object).readPointer();
|
|
43
|
+
const begin = d.add(8).readS32(), end = d.add(12).readS32();
|
|
44
|
+
if (begin < 0 || end < begin || end - begin > 5000) throw new Error('Unexpected QList layout');
|
|
45
|
+
const result = [];
|
|
46
|
+
for (let i = begin; i < end; i++) result.push(d.add(16 + i * Process.pointerSize).readPointer());
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function label(object) {
|
|
51
|
+
const result = Memory.alloc(Process.pointerSize);
|
|
52
|
+
buttonText(object, result);
|
|
53
|
+
try { return stringData(result).readUtf16String(stringSize(result)); }
|
|
54
|
+
finally { stringDtor(result); }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function walk(root) {
|
|
58
|
+
const result = [], queue = [{object: root, parent: null}], seen = new Set();
|
|
59
|
+
while (queue.length) {
|
|
60
|
+
const item = queue.shift(), object = item.object;
|
|
61
|
+
if (object.isNull() || seen.has(object.toString())) continue;
|
|
62
|
+
seen.add(object.toString());
|
|
63
|
+
if (seen.size > 5000) throw new Error('Object tree too large');
|
|
64
|
+
const isWidget = hasType(object, 'QWidget');
|
|
65
|
+
const row = {address: object.toString(), parent: item.parent, class_name: typeName(object)};
|
|
66
|
+
if (isWidget) {
|
|
67
|
+
row.visible = isVisible(object);
|
|
68
|
+
row.enabled = isEnabled(object);
|
|
69
|
+
if (hasType(object, 'QLineEdit')) row.echo_mode = echoMode(object);
|
|
70
|
+
if (hasType(object, 'QAbstractButton')) row.label = label(object);
|
|
71
|
+
}
|
|
72
|
+
result.push(row);
|
|
73
|
+
childObjects(object).forEach(child => queue.push({object: child, parent: object.toString()}));
|
|
74
|
+
}
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getRoot(hwnd) {
|
|
79
|
+
const root = findWidget(uint64(hwnd));
|
|
80
|
+
if (root.isNull()) throw new Error('No Qt widget for target window');
|
|
81
|
+
return root;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function passwordEditor(root, requireEnabled = true) {
|
|
85
|
+
if (!hasType(root, 'TC::CLoginDialog')) throw new Error('Not a QMT login dialog');
|
|
86
|
+
const rows = walk(root);
|
|
87
|
+
const edits = rows.filter(row => row.echo_mode === 2 && row.visible && (!requireEnabled || row.enabled));
|
|
88
|
+
if (edits.length !== 1) throw new Error('Expected exactly one visible password editor');
|
|
89
|
+
return {rows, editor: ptr(edits[0].address)};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function hasText(editor) {
|
|
93
|
+
const value = Memory.alloc(Process.pointerSize);
|
|
94
|
+
editText(editor, value);
|
|
95
|
+
// Only return emptiness; never copy a remembered password out of the process.
|
|
96
|
+
try { return stringSize(value) > 0; }
|
|
97
|
+
finally { stringDtor(value); }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function noticeKind(rows) {
|
|
101
|
+
let credential = false, network = false;
|
|
102
|
+
for (const row of rows) {
|
|
103
|
+
const object = ptr(row.address);
|
|
104
|
+
if (!row.visible || !hasType(object, 'QLabel')) continue;
|
|
105
|
+
const value = Memory.alloc(Process.pointerSize);
|
|
106
|
+
labelText(object, value);
|
|
107
|
+
let text;
|
|
108
|
+
try { text = stringData(value).readUtf16String(stringSize(value)); }
|
|
109
|
+
finally { stringDtor(value); }
|
|
110
|
+
if (/验证码|动态口令|短信验证|captcha/i.test(text)) return 'challenge';
|
|
111
|
+
if (/密码.{0,12}(错误|不正确|失败|无效)|口令.{0,12}(错误|不正确|失败)|password.{0,16}(incorrect|invalid|wrong)/i.test(text)) credential = true;
|
|
112
|
+
if (/(网络|连接|服务器).{0,16}(失败|超时|异常)|network.{0,16}(error|timeout)/i.test(text)) network = true;
|
|
113
|
+
}
|
|
114
|
+
return credential ? 'credential_error' : network ? 'network_error' : null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function submit(root) {
|
|
118
|
+
passwordEditor(root);
|
|
119
|
+
const login = fn(Process.getModuleByName('LoginDialog.dll'),
|
|
120
|
+
'?slot_loginClicked@CLoginDialog@TC@@QEAAXXZ', 'void', ['pointer']);
|
|
121
|
+
login(root);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function loginIdentity(root) {
|
|
125
|
+
const getName = fn(Process.getModuleByName('LoginDialog.dll'),
|
|
126
|
+
'?getUserName@CLoginDialog@TC@@IEAA?AVQString@@XZ', 'pointer', ['pointer', 'pointer']);
|
|
127
|
+
const value = Memory.alloc(Process.pointerSize);
|
|
128
|
+
getName(root, value);
|
|
129
|
+
try { return stringData(value).readUtf16String(stringSize(value)); }
|
|
130
|
+
finally { stringDtor(value); }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function onGui(hwnd, operation) {
|
|
134
|
+
if (pending) throw new Error('An operation is already pending');
|
|
135
|
+
const thread = getThread(ptr(hwnd), NULL);
|
|
136
|
+
if (!thread) throw new Error('Target window no longer exists');
|
|
137
|
+
return new Promise((resolve, reject) => {
|
|
138
|
+
const timer = setTimeout(() => {
|
|
139
|
+
if (pending && pending.resolve === resolve) {
|
|
140
|
+
pending = null;
|
|
141
|
+
reject(new Error('GUI operation timeout'));
|
|
142
|
+
}
|
|
143
|
+
}, 8000);
|
|
144
|
+
pending = {thread, operation, resolve, reject, timer};
|
|
145
|
+
// Wake the normal message loop without mouse/keyboard events or foreground changes.
|
|
146
|
+
postMessage(ptr(hwnd), 0, NULL, NULL);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
Interceptor.attach(user32.getExportByName('DispatchMessageW'), {
|
|
151
|
+
onLeave() {
|
|
152
|
+
if (!pending || this.threadId !== pending.thread) return;
|
|
153
|
+
const job = pending;
|
|
154
|
+
pending = null;
|
|
155
|
+
clearTimeout(job.timer);
|
|
156
|
+
try { job.resolve({gui_thread: this.threadId, result: job.operation()}); }
|
|
157
|
+
catch (error) { job.reject(error); }
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
rpc.exports = {
|
|
162
|
+
selectaccount(hwnd, account, replace = false) {
|
|
163
|
+
return onGui(hwnd, () => {
|
|
164
|
+
const root = getRoot(hwnd);
|
|
165
|
+
passwordEditor(root);
|
|
166
|
+
if (!replace && loginIdentity(root) === account) return {changed: false};
|
|
167
|
+
const candidates = walk(root).filter(row => row.visible && row.enabled &&
|
|
168
|
+
hasType(ptr(row.address), 'QComboBox') && comboEditable(ptr(row.address)));
|
|
169
|
+
if (candidates.length !== 1) throw new Error('No unique editable account selector');
|
|
170
|
+
if (replace) {
|
|
171
|
+
const empty = Memory.alloc(Process.pointerSize);
|
|
172
|
+
stringCtor(empty, Memory.allocUtf16String(''), 0);
|
|
173
|
+
try { comboSetText(ptr(candidates[0].address), empty); }
|
|
174
|
+
finally { stringDtor(empty); }
|
|
175
|
+
}
|
|
176
|
+
const data = Memory.allocUtf16String(account), value = Memory.alloc(Process.pointerSize);
|
|
177
|
+
stringCtor(value, data, account.length);
|
|
178
|
+
try { comboSetText(ptr(candidates[0].address), value); }
|
|
179
|
+
finally { stringDtor(value); }
|
|
180
|
+
if (loginIdentity(root) !== account) throw new Error('Could not select configured login account');
|
|
181
|
+
return {changed: true};
|
|
182
|
+
});
|
|
183
|
+
},
|
|
184
|
+
inspect(hwnd) { return onGui(hwnd, () => walk(getRoot(hwnd))); },
|
|
185
|
+
status(hwnd) {
|
|
186
|
+
return onGui(hwnd, () => {
|
|
187
|
+
const root = getRoot(hwnd);
|
|
188
|
+
if (!hasType(root, 'TC::CLoginDialog')) return {is_login: false, is_main: hasType(root, 'TC::CMainWindow')};
|
|
189
|
+
const rows = walk(root);
|
|
190
|
+
const edits = rows.filter(row => row.echo_mode === 2 && row.visible);
|
|
191
|
+
if (edits.length > 1) throw new Error('Multiple password editors are visible');
|
|
192
|
+
return {is_login: true, has_password: edits.length === 1 && hasText(ptr(edits[0].address)),
|
|
193
|
+
busy: edits.length === 0, login_identity: loginIdentity(root), notice: noticeKind(rows),
|
|
194
|
+
login_enabled: rows.some(row => row.visible && row.enabled && /^登\s*录(?:[((]\d+[))])?$/.test(row.label || ''))};
|
|
195
|
+
});
|
|
196
|
+
},
|
|
197
|
+
notice(hwnd) {
|
|
198
|
+
return onGui(hwnd, () => {
|
|
199
|
+
const rows = walk(getRoot(hwnd));
|
|
200
|
+
return {kind: noticeKind(rows)};
|
|
201
|
+
});
|
|
202
|
+
},
|
|
203
|
+
acknowledge(hwnd) {
|
|
204
|
+
return onGui(hwnd, () => {
|
|
205
|
+
const rows = walk(getRoot(hwnd));
|
|
206
|
+
if (noticeKind(rows) !== 'credential_error') throw new Error('Not a recognized credential failure');
|
|
207
|
+
const buttons = rows.filter(row => row.visible && row.enabled && /^(确定|确认|OK|&OK)$/.test(row.label || ''));
|
|
208
|
+
if (buttons.length !== 1) throw new Error('No unique acknowledgement button');
|
|
209
|
+
buttonClick(ptr(buttons[0].address));
|
|
210
|
+
return {acknowledged: true};
|
|
211
|
+
});
|
|
212
|
+
},
|
|
213
|
+
submit(hwnd) { return onGui(hwnd, () => { submit(getRoot(hwnd)); return {submitted: true}; }); },
|
|
214
|
+
close(hwnd) { return onGui(hwnd, () => ({closed: closeWidget(getRoot(hwnd))})); },
|
|
215
|
+
clicklabel(hwnd, text) {
|
|
216
|
+
return onGui(hwnd, () => {
|
|
217
|
+
const rows = walk(getRoot(hwnd)).filter(row => row.label === text && row.visible && row.enabled);
|
|
218
|
+
if (rows.length !== 1) throw new Error('Expected one matching enabled button');
|
|
219
|
+
buttonClick(ptr(rows[0].address));
|
|
220
|
+
return {clicked: true};
|
|
221
|
+
});
|
|
222
|
+
},
|
|
223
|
+
fill(hwnd, value, submit) {
|
|
224
|
+
return onGui(hwnd, () => {
|
|
225
|
+
const root = getRoot(hwnd);
|
|
226
|
+
if (!hasType(root, 'TC::CLoginDialog')) throw new Error('Not a QMT login dialog');
|
|
227
|
+
const rows = walk(root);
|
|
228
|
+
const edits = rows.filter(row => row.echo_mode === 2 && row.visible && row.enabled);
|
|
229
|
+
if (edits.length !== 1) throw new Error('Expected exactly one visible password editor');
|
|
230
|
+
const text = Memory.allocUtf16String(value);
|
|
231
|
+
const string = Memory.alloc(Process.pointerSize);
|
|
232
|
+
stringCtor(string, text, value.length);
|
|
233
|
+
try { setText(ptr(edits[0].address), string); }
|
|
234
|
+
finally {
|
|
235
|
+
stringDtor(string);
|
|
236
|
+
text.writeByteArray(new Uint8Array((value.length + 1) * 2));
|
|
237
|
+
}
|
|
238
|
+
const count = value.length;
|
|
239
|
+
value = null;
|
|
240
|
+
if (submit) {
|
|
241
|
+
const login = fn(Process.getModuleByName('LoginDialog.dll'),
|
|
242
|
+
'?slot_loginClicked@CLoginDialog@TC@@QEAAXXZ', 'void', ['pointer']);
|
|
243
|
+
login(root);
|
|
244
|
+
}
|
|
245
|
+
return {filled: true, submitted: Boolean(submit), characters: count,
|
|
246
|
+
input_method: 'QLineEdit::setText', login_method: submit ? 'CLoginDialog::slot_loginClicked' : null};
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
};
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""Explicit QMT component login for a configured binding (Windows, Qt 5.6.2 x64).
|
|
2
|
+
|
|
3
|
+
Run with --binding NAME. Password is requested only if needed, using --prompt
|
|
4
|
+
or stdin. This tool never starts/stops QMT; it replaces the login form contents.
|
|
5
|
+
The web auto-start service also uses login_running_qmt after starting a process.
|
|
6
|
+
"""
|
|
7
|
+
import argparse
|
|
8
|
+
import getpass
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class QmtNotReady(RuntimeError):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def verify_environment(directory):
|
|
20
|
+
"""Validate dependencies and native ABI before stopping a running terminal."""
|
|
21
|
+
try:
|
|
22
|
+
from .qmt_dependencies import LOGIN_MODULES, import_login_module
|
|
23
|
+
except ImportError:
|
|
24
|
+
from qmt_dependencies import LOGIN_MODULES, import_login_module
|
|
25
|
+
modules = {name: import_login_module(name) for name in LOGIN_MODULES}
|
|
26
|
+
win32api = modules["win32api"]
|
|
27
|
+
directory = Path(directory)
|
|
28
|
+
for module in ("Qt5Core.dll", "Qt5Widgets.dll"):
|
|
29
|
+
version = win32api.GetFileVersionInfo(str(directory / module), "\\")
|
|
30
|
+
if (version["FileVersionMS"], version["FileVersionLS"]) != ((5 << 16) | 6, 2 << 16):
|
|
31
|
+
raise RuntimeError("QMT 组件自动登录当前仅支持 Qt 5.6.2,请先适配此客户端版本")
|
|
32
|
+
with (directory / "XtItClient.exe").open("rb") as stream:
|
|
33
|
+
stream.seek(0x3C)
|
|
34
|
+
offset = int.from_bytes(stream.read(4), "little")
|
|
35
|
+
stream.seek(offset + 4)
|
|
36
|
+
if int.from_bytes(stream.read(2), "little") != 0x8664:
|
|
37
|
+
raise RuntimeError("QMT 组件自动登录需要 64 位客户端")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def auto_login(backend, password_provider):
|
|
41
|
+
"""Replace existing password contents and submit the supplied credential once."""
|
|
42
|
+
attempts = []
|
|
43
|
+
password_requested = False
|
|
44
|
+
|
|
45
|
+
def result(outcome):
|
|
46
|
+
return {"outcome": outcome, "login_succeeded": outcome in ("logged_in", "already_logged_in"),
|
|
47
|
+
"attempts": attempts, "password_requested": password_requested}
|
|
48
|
+
|
|
49
|
+
if backend.already_logged_in():
|
|
50
|
+
return result("already_logged_in")
|
|
51
|
+
state = backend.state()
|
|
52
|
+
if state.get("busy"):
|
|
53
|
+
return result("login_not_ready")
|
|
54
|
+
if state.get("notice") in ("challenge", "network_error"):
|
|
55
|
+
return result(state["notice"])
|
|
56
|
+
if state.get("notice") == "credential_error":
|
|
57
|
+
backend.acknowledge_failure()
|
|
58
|
+
password_requested = True
|
|
59
|
+
password = password_provider()
|
|
60
|
+
if not password:
|
|
61
|
+
return result("password_required")
|
|
62
|
+
try:
|
|
63
|
+
backend.fill(password)
|
|
64
|
+
finally:
|
|
65
|
+
password = None
|
|
66
|
+
if not backend.state()["login_enabled"]:
|
|
67
|
+
return result("login_not_ready")
|
|
68
|
+
backend.submit()
|
|
69
|
+
attempts.append("provided")
|
|
70
|
+
return result(backend.wait_outcome())
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def select_binding(config_path, name):
|
|
74
|
+
config = json.loads(Path(config_path).read_text(encoding="utf-8"))
|
|
75
|
+
rows = config.get("account_configs") or {}
|
|
76
|
+
rows = rows.values() if isinstance(rows, dict) else rows
|
|
77
|
+
matches = [row for row in rows if row.get("display_name") == name]
|
|
78
|
+
if len(matches) != 1:
|
|
79
|
+
raise ValueError("Expected exactly one binding with that display name")
|
|
80
|
+
root = Path(matches[0]["qmt_dir"]).resolve()
|
|
81
|
+
for directory in (root, root / "bin.x64"):
|
|
82
|
+
if (directory / "XtItClient.exe").is_file():
|
|
83
|
+
return matches[0], directory
|
|
84
|
+
raise ValueError("Binding directory does not contain XtItClient.exe")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class QtLoginBackend:
|
|
88
|
+
def __init__(self, directory, timeout=40, expected_pid=None):
|
|
89
|
+
try:
|
|
90
|
+
from .qmt_dependencies import import_login_module
|
|
91
|
+
except ImportError:
|
|
92
|
+
from qmt_dependencies import import_login_module
|
|
93
|
+
import psutil
|
|
94
|
+
frida = import_login_module("frida")
|
|
95
|
+
win32api = import_login_module("win32api")
|
|
96
|
+
win32event = import_login_module("win32event")
|
|
97
|
+
win32gui = import_login_module("win32gui")
|
|
98
|
+
win32process = import_login_module("win32process")
|
|
99
|
+
self.gui, self.process = win32gui, win32process
|
|
100
|
+
self.timeout = timeout
|
|
101
|
+
self.hwnd = None
|
|
102
|
+
self.identity = None
|
|
103
|
+
self.failure_hwnd = None
|
|
104
|
+
self.session = None
|
|
105
|
+
self.mutex = None
|
|
106
|
+
self.messages = []
|
|
107
|
+
self.foreground_before = win32gui.GetForegroundWindow()
|
|
108
|
+
expected = (directory / "XtItClient.exe").resolve()
|
|
109
|
+
matches = []
|
|
110
|
+
for process in psutil.process_iter(["name"]):
|
|
111
|
+
try:
|
|
112
|
+
if (process.info["name"] or "").lower() == "xtitclient.exe" and Path(process.exe()).resolve() == expected:
|
|
113
|
+
matches.append(process)
|
|
114
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
115
|
+
continue
|
|
116
|
+
if not matches:
|
|
117
|
+
raise QmtNotReady("QMT process is not ready")
|
|
118
|
+
if len(matches) != 1:
|
|
119
|
+
raise RuntimeError("Expected exactly one running QMT for this binding directory")
|
|
120
|
+
self.pid = matches[0].pid
|
|
121
|
+
if expected_pid is not None and self.pid != expected_pid:
|
|
122
|
+
raise RuntimeError("QMT process changed; stopping this login attempt")
|
|
123
|
+
verify_environment(directory)
|
|
124
|
+
self.mutex = win32event.CreateMutex(None, False, "Local\\CFQuantQmtComponentLogin_" + str(self.pid))
|
|
125
|
+
acquired = win32event.WaitForSingleObject(self.mutex, 0)
|
|
126
|
+
if acquired not in (0, 0x80):
|
|
127
|
+
win32api.CloseHandle(self.mutex)
|
|
128
|
+
self.mutex = None
|
|
129
|
+
raise RuntimeError("Another component login is already running for this QMT")
|
|
130
|
+
try:
|
|
131
|
+
self.session = frida.attach(self.pid)
|
|
132
|
+
readiness = self.session.create_script("rpc.exports.ready = () => ['Qt5Core.dll', 'Qt5Widgets.dll', 'LoginDialog.dll'].every(n => Process.findModuleByName(n) !== null);")
|
|
133
|
+
readiness.load()
|
|
134
|
+
ready = readiness.exports_sync.ready()
|
|
135
|
+
readiness.unload()
|
|
136
|
+
if not ready:
|
|
137
|
+
raise QmtNotReady("QMT login modules are still loading")
|
|
138
|
+
script = self.session.create_script(Path(__file__).with_name("qmt_component_agent.js").read_text(encoding="utf-8"))
|
|
139
|
+
script.on("message", lambda message, data: self.messages.append("agent_error") if message.get("type") == "error" else None)
|
|
140
|
+
script.load()
|
|
141
|
+
self.script, self.api = script, script.exports_sync
|
|
142
|
+
self.existing_main = False
|
|
143
|
+
self.main_hwnd = None
|
|
144
|
+
for hwnd in self.windows():
|
|
145
|
+
state = self.api.status(str(hwnd))["result"]
|
|
146
|
+
if state.get("is_login"):
|
|
147
|
+
if self.hwnd is not None:
|
|
148
|
+
raise RuntimeError("Multiple login dialogs found")
|
|
149
|
+
self.hwnd = hwnd
|
|
150
|
+
self.identity = state["login_identity"]
|
|
151
|
+
elif state.get("is_main"):
|
|
152
|
+
self.existing_main = True
|
|
153
|
+
self.main_hwnd = hwnd
|
|
154
|
+
if self.hwnd is None and not self.existing_main:
|
|
155
|
+
raise QmtNotReady("No supported login or main window found yet")
|
|
156
|
+
self.minimized_before = bool(self.hwnd and self.gui.IsIconic(self.hwnd))
|
|
157
|
+
except Exception:
|
|
158
|
+
self.close()
|
|
159
|
+
raise
|
|
160
|
+
|
|
161
|
+
def windows(self):
|
|
162
|
+
result = []
|
|
163
|
+
def visit(hwnd, _):
|
|
164
|
+
if self.process.GetWindowThreadProcessId(hwnd)[1] == self.pid and self.gui.IsWindowVisible(hwnd) and self.gui.GetClassName(hwnd).startswith("Qt"):
|
|
165
|
+
result.append(hwnd)
|
|
166
|
+
self.gui.EnumWindows(visit, None)
|
|
167
|
+
return result
|
|
168
|
+
|
|
169
|
+
def already_logged_in(self):
|
|
170
|
+
return self.existing_main and self.hwnd is None
|
|
171
|
+
|
|
172
|
+
def state(self):
|
|
173
|
+
state = self.api.status(str(self.hwnd))["result"]
|
|
174
|
+
if not state.get("is_login") or not self.identity or state.get("login_identity") != self.identity:
|
|
175
|
+
raise RuntimeError("Login window or selected account changed; stopping")
|
|
176
|
+
state.pop("login_identity", None)
|
|
177
|
+
for hwnd in self.windows():
|
|
178
|
+
if hwnd != self.hwnd and hwnd != self.main_hwnd:
|
|
179
|
+
kind = self.api.notice(str(hwnd))["result"].get("kind")
|
|
180
|
+
if kind:
|
|
181
|
+
state["notice"] = kind
|
|
182
|
+
self.failure_hwnd = hwnd
|
|
183
|
+
break
|
|
184
|
+
return state
|
|
185
|
+
|
|
186
|
+
def select_account(self, account, replace=False):
|
|
187
|
+
if self.already_logged_in():
|
|
188
|
+
if account and not self.gui.GetWindowText(self.main_hwnd).startswith(account + " - "):
|
|
189
|
+
raise RuntimeError("QMT 已登录的用户与配置的登录账号不一致,请重启该绑定 QMT")
|
|
190
|
+
return
|
|
191
|
+
if account and (replace or account != self.identity):
|
|
192
|
+
self.api.selectaccount(str(self.hwnd), account, replace)
|
|
193
|
+
self.identity = account
|
|
194
|
+
# Managed login clears remembered contents even for the same user.
|
|
195
|
+
self.api.fill(str(self.hwnd), "", False)
|
|
196
|
+
|
|
197
|
+
def submit(self):
|
|
198
|
+
state = self.state()
|
|
199
|
+
if not state["has_password"] or not state["login_enabled"]:
|
|
200
|
+
raise RuntimeError("Login is not ready")
|
|
201
|
+
self.api.submit(str(self.hwnd))
|
|
202
|
+
|
|
203
|
+
def fill(self, password):
|
|
204
|
+
self.state()
|
|
205
|
+
self.api.fill(str(self.hwnd), password, False)
|
|
206
|
+
|
|
207
|
+
def wait_outcome(self):
|
|
208
|
+
deadline = time.monotonic() + self.timeout
|
|
209
|
+
while time.monotonic() < deadline:
|
|
210
|
+
windows = self.windows()
|
|
211
|
+
for hwnd in windows:
|
|
212
|
+
caption = self.gui.GetWindowText(hwnd)
|
|
213
|
+
if self.identity and caption.startswith(self.identity + " - "):
|
|
214
|
+
if self.api.status(str(hwnd))["result"].get("is_main"):
|
|
215
|
+
return "logged_in"
|
|
216
|
+
for hwnd in windows:
|
|
217
|
+
if hwnd == self.hwnd:
|
|
218
|
+
try:
|
|
219
|
+
state = self.state()
|
|
220
|
+
except Exception:
|
|
221
|
+
if not self.gui.IsWindow(hwnd) or not self.gui.IsWindowVisible(hwnd):
|
|
222
|
+
continue
|
|
223
|
+
raise
|
|
224
|
+
kind = state.get("notice")
|
|
225
|
+
if not state["login_enabled"]:
|
|
226
|
+
continue
|
|
227
|
+
else:
|
|
228
|
+
kind = self.api.notice(str(hwnd))["result"].get("kind")
|
|
229
|
+
if kind:
|
|
230
|
+
self.failure_hwnd = hwnd
|
|
231
|
+
return kind
|
|
232
|
+
time.sleep(0.5)
|
|
233
|
+
# A slow connection is not evidence of a wrong password. Never resubmit.
|
|
234
|
+
return "timeout"
|
|
235
|
+
|
|
236
|
+
def acknowledge_failure(self):
|
|
237
|
+
if self.failure_hwnd and self.failure_hwnd != self.hwnd:
|
|
238
|
+
self.api.acknowledge(str(self.failure_hwnd))
|
|
239
|
+
self.failure_hwnd = None
|
|
240
|
+
|
|
241
|
+
def close(self):
|
|
242
|
+
try:
|
|
243
|
+
from .qmt_dependencies import import_login_module
|
|
244
|
+
except ImportError:
|
|
245
|
+
from qmt_dependencies import import_login_module
|
|
246
|
+
win32api = import_login_module("win32api")
|
|
247
|
+
win32event = import_login_module("win32event")
|
|
248
|
+
try:
|
|
249
|
+
if self.session:
|
|
250
|
+
self.session.detach()
|
|
251
|
+
self.session = None
|
|
252
|
+
finally:
|
|
253
|
+
if self.mutex:
|
|
254
|
+
win32event.ReleaseMutex(self.mutex)
|
|
255
|
+
win32api.CloseHandle(self.mutex)
|
|
256
|
+
self.mutex = None
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def main():
|
|
260
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
261
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
262
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
263
|
+
parser.add_argument("--binding", required=True)
|
|
264
|
+
parser.add_argument("--config", type=Path, default=Path(__file__).resolve().parents[1] / "runtime/config/cfquant_web_config.json")
|
|
265
|
+
parser.add_argument("--prompt", action="store_true", help="Read a needed password without echo; otherwise read one line of stdin")
|
|
266
|
+
parser.add_argument("--timeout", type=float, default=40)
|
|
267
|
+
parser.add_argument("--report", type=Path)
|
|
268
|
+
args = parser.parse_args()
|
|
269
|
+
if not 1 <= args.timeout <= 120:
|
|
270
|
+
parser.error("timeout must be between 1 and 120 seconds")
|
|
271
|
+
row, directory = select_binding(args.config, args.binding)
|
|
272
|
+
backend = QtLoginBackend(directory, args.timeout)
|
|
273
|
+
try:
|
|
274
|
+
backend.select_account((row.get("qmt_auto_login") or {}).get("login_account", ""), replace=True)
|
|
275
|
+
def read_password():
|
|
276
|
+
return getpass.getpass("QMT password (hidden): ") if args.prompt else sys.stdin.readline().rstrip("\r\n")
|
|
277
|
+
result = auto_login(backend, read_password)
|
|
278
|
+
result.update(binding=args.binding, process_id=backend.pid,
|
|
279
|
+
minimized_before=backend.minimized_before,
|
|
280
|
+
foreground_before=backend.foreground_before,
|
|
281
|
+
foreground_after=backend.gui.GetForegroundWindow(),
|
|
282
|
+
script_errors=backend.messages,
|
|
283
|
+
input_method="Qt native methods; no keyboard/mouse input")
|
|
284
|
+
output = json.dumps(result, ensure_ascii=False, indent=2)
|
|
285
|
+
if args.report:
|
|
286
|
+
args.report.write_text(output, encoding="utf-8")
|
|
287
|
+
print(output)
|
|
288
|
+
return 0 if result["outcome"] in ("logged_in", "already_logged_in") else 1
|
|
289
|
+
finally:
|
|
290
|
+
backend.close()
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def login_running_qmt(directory, login_account="", password_provider=lambda: "", expected_pid=None,
|
|
294
|
+
startup_timeout=60, timeout=40):
|
|
295
|
+
"""Wait for a freshly launched QMT, then execute one bounded login flow."""
|
|
296
|
+
deadline = time.monotonic() + startup_timeout
|
|
297
|
+
while True:
|
|
298
|
+
try:
|
|
299
|
+
backend = QtLoginBackend(Path(directory), timeout, expected_pid=expected_pid)
|
|
300
|
+
break
|
|
301
|
+
except QmtNotReady:
|
|
302
|
+
if time.monotonic() >= deadline:
|
|
303
|
+
return {"outcome": "startup_timeout", "login_succeeded": False, "attempts": []}
|
|
304
|
+
time.sleep(0.5)
|
|
305
|
+
try:
|
|
306
|
+
if not backend.already_logged_in() and backend.state().get("busy"):
|
|
307
|
+
outcome = backend.wait_outcome()
|
|
308
|
+
if outcome == "logged_in":
|
|
309
|
+
# Reopen only the automation session to verify the resulting user.
|
|
310
|
+
backend.close()
|
|
311
|
+
backend = QtLoginBackend(Path(directory), timeout, expected_pid=expected_pid)
|
|
312
|
+
elif outcome != "credential_error":
|
|
313
|
+
return {"outcome": outcome, "login_succeeded": False, "attempts": []}
|
|
314
|
+
backend.select_account(login_account, replace=True)
|
|
315
|
+
return auto_login(backend, password_provider)
|
|
316
|
+
finally:
|
|
317
|
+
backend.close()
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
if __name__ == "__main__":
|
|
321
|
+
sys.exit(main())
|