cfquant 0.2.54__tar.gz → 0.2.56__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.54 → cfquant-0.2.56}/PKG-INFO +3 -1
- {cfquant-0.2.54 → cfquant-0.2.56}/README.md +5 -3
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/_editable_install.py +7 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/batch_orders.py +15 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/cftrader.py +21 -3
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/client.py +13 -3
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/normal_bridge.py +24 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/pipe_client.py +20 -13
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/protocol.py +5 -4
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/qmt_component_login.py +21 -10
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/qmt_credentials.py +8 -2
- cfquant-0.2.56/cfquant/qmt_dependencies.py +158 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/qmt_pipe_runtime.py +28 -5
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/qmt_strategy_deploy.py +66 -35
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/rpc_timing.py +15 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/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 +150 -0
- cfquant-0.2.56/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 +170 -0
- cfquant-0.2.56/cfquant/tests/test_account_query_timing.py +131 -0
- cfquant-0.2.56/cfquant/tests/test_binding_live_refresh_browser.py +95 -0
- cfquant-0.2.56/cfquant/tests/test_callback_recovery.py +287 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_cftrader.py +17 -1
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_cftrader_tutorial.py +5 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_market_timing.py +1 -1
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_qmt_auto_login_browser.py +24 -0
- cfquant-0.2.56/cfquant/tests/test_qmt_dependencies.py +107 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_qmt_fileless.py +129 -3
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_qmt_strategy_browser.py +14 -6
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_qmt_strategy_deploy.py +29 -0
- cfquant-0.2.56/cfquant/tests/test_remote_version_check.py +72 -0
- cfquant-0.2.56/cfquant/tests/test_trader_subscription_recovery.py +345 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_tutorial_reader.py +3 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tx_trade_bridge.py +36 -12
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/version.py +2 -2
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/xttrader.py +201 -43
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant.egg-info/PKG-INFO +3 -1
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant.egg-info/SOURCES.txt +7 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant_web_server.py +494 -246
- {cfquant-0.2.54 → cfquant-0.2.56}/pyproject.toml +1 -1
- {cfquant-0.2.54 → cfquant-0.2.56}/qmt_scripts/CFQUANT_LITE.py +102 -15
- {cfquant-0.2.54 → cfquant-0.2.56}/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 +102 -15
- {cfquant-0.2.54 → cfquant-0.2.56}/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 +102 -15
- {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/app.js +276 -53
- {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/index.html +20 -5
- {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/styles.css +57 -6
- cfquant-0.2.54/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 -88
- {cfquant-0.2.54 → cfquant-0.2.56}/LICENSE +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/LTtx/__init__.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/LTtx/tx/Config.txt +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/LTtx/tx/LTtx_server.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/LTtx/tx/__init__.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/LTtx/tx/tx.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/MANIFEST.in +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/__init__.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/account_routing.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/build_info.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/callback_dispatcher.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/channels.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/cli.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/config.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/diagnostics.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/flow_control.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/hub.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/level2.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/log_management.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/logging_i18n.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/management.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/order_identity.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/order_meta.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/pipe_bridge.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/pipe_hub.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/pipe_transport.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/qmt_binding_settings.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/qmt_bridge.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/qmt_component_agent.js +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/qmt_strategy_package.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/qmt_strategy_runtime.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/quote_history.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/runtime_report.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/stock_connect.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/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.54 → cfquant-0.2.56}/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.54 → cfquant-0.2.56}/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.54 → cfquant-0.2.56}/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.54 → cfquant-0.2.56}/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.54 → cfquant-0.2.56}/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.54 → cfquant-0.2.56}/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.54 → cfquant-0.2.56}/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.54 → cfquant-0.2.56}/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.54 → cfquant-0.2.56}/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.54 → cfquant-0.2.56}/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.54 → cfquant-0.2.56}/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.54 → cfquant-0.2.56}/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.54 → cfquant-0.2.56}/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.54 → cfquant-0.2.56}/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.54 → cfquant-0.2.56}/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.54 → cfquant-0.2.56}/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.54 → cfquant-0.2.56}/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.54 → cfquant-0.2.56}/cfquant/tests/29_Level2/350/241/214/346/203/205/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/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.54 → cfquant-0.2.56}/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.54 → cfquant-0.2.56}/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.54 → cfquant-0.2.56}/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.54 → cfquant-0.2.56}/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.54 → cfquant-0.2.56}/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.54 → cfquant-0.2.56}/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.54 → cfquant-0.2.56}/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.54 → cfquant-0.2.56}/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.54 → cfquant-0.2.56}/cfquant/tests/6_/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/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.54 → cfquant-0.2.56}/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.54 → cfquant-0.2.56}/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.54 → cfquant-0.2.56}/cfquant/tests/_helpers.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/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.54 → cfquant-0.2.56}/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.54 → cfquant-0.2.56}/cfquant/tests/readme.md +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_advanced_parity.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_api_tester_browser.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_api_tester_web.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_binding_runtime_browser.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_callback_requests.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_cftrader_web.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_credit_compat.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_dataframe_encoding.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_json_serialization.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_latency_flow_control.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_lite_account_routing.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_lite_deployment_paths.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_lite_parity.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_lite_qmt_dispatch.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_local_trading_calendar.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_log_management.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_management.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_management_batch.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_market_data_fallback.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_order_identity.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_order_time_callback.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_project_updater.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_python_api_reference.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_qmt_auto_login_integration.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_qmt_binding_settings.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_qmt_component_login.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_qmt_strategy_web.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_quote_latency.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_quote_latency_encoding.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_startup_recovery.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_stock_connect.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_stock_connect_browser.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_table_conversion.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_trade_strategy_callback.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_trading_dates.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_tx_auto_import.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_tx_shutdown.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/trading_calendar.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tx.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/xtconstant.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/xtdata.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/xttype.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant.egg-info/dependency_links.txt +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant.egg-info/entry_points.txt +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant.egg-info/requires.txt +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant.egg-info/top_level.txt +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/cfquant_pipe_hub.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/qmt_scripts/CFQUANT.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/qmt_scripts/__init__.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/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.54 → cfquant-0.2.56}/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.54 → cfquant-0.2.56}/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.54 → cfquant-0.2.56}/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.54 → cfquant-0.2.56}/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.54 → cfquant-0.2.56}/requirements.txt +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/setup.cfg +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/__init__.py +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/api-tester.js +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/avatars/amber-pulse.svg +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/avatars/copper-grid.svg +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/avatars/market-blue.svg +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/avatars/rose-circuit.svg +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/avatars/signal-green.svg +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/avatars/slate-wave.svg +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/avatars/teal-orbit.svg +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/avatars/violet-node.svg +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/cftrader-api-data.js +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/docs.js +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/favicon.svg +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/python-api-data.js +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/python-api.js +0 -0
- {cfquant-0.2.54 → cfquant-0.2.56}/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.56
|
|
4
4
|
Summary: QMT local bridge and xtquant-compatible Python client.
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.8
|
|
@@ -343,6 +343,8 @@ trader.disconnect()
|
|
|
343
343
|
|
|
344
344
|
`XtQuantTrader`、`StockAccount`、常用行情查询和交易方法保持接近 `xtquant` 的调用方式,原有回调类通常可以继续使用。仍有部分 QMT 专有接口、字段和返回结构存在兼容边界,使用前请查看 [xtquant 原版接口适配清单](docs/xtquant原版接口适配清单.md) 和 [QMT 函数封装能力清单](docs/QMT函数封装能力清单.md)。
|
|
345
345
|
|
|
346
|
+
账号订阅成功后,SDK 会在后台约每两秒重新确认订阅。用户程序保持运行时,QMT、cfquant Web 或通信服务重启后,待桥接策略恢复授权、通道可用,SDK 会自动恢复该账号的新回调;默认 Web 路由和直接 Pipe/LTtx 连接均适用。主动 `unsubscribe(account)` 后不会再恢复该账号,`stop()` / `disconnect()` 会停止后台恢复。此机制只恢复账号订阅,不补发停机期间的回调,也不重试委托或撤单。升级 SDK 后需先重启一次用户程序加载新代码,此后无需随 QMT 重启用户程序。
|
|
347
|
+
|
|
346
348
|
推荐迁移顺序:**行情查询 → 资金/持仓 → 回调 → 模拟委托 → 模拟撤单 → 正式策略**。每一步都先在 Web 控制台接口测试和日志中确认,再进入下一步。
|
|
347
349
|
|
|
348
350
|
## Web 控制台
|
|
@@ -317,9 +317,11 @@ trader.disconnect()
|
|
|
317
317
|
|
|
318
318
|
### 回调和接口差异
|
|
319
319
|
|
|
320
|
-
`XtQuantTrader`、`StockAccount`、常用行情查询和交易方法保持接近 `xtquant` 的调用方式,原有回调类通常可以继续使用。仍有部分 QMT 专有接口、字段和返回结构存在兼容边界,使用前请查看 [xtquant 原版接口适配清单](docs/xtquant原版接口适配清单.md) 和 [QMT 函数封装能力清单](docs/QMT函数封装能力清单.md)。
|
|
321
|
-
|
|
322
|
-
|
|
320
|
+
`XtQuantTrader`、`StockAccount`、常用行情查询和交易方法保持接近 `xtquant` 的调用方式,原有回调类通常可以继续使用。仍有部分 QMT 专有接口、字段和返回结构存在兼容边界,使用前请查看 [xtquant 原版接口适配清单](docs/xtquant原版接口适配清单.md) 和 [QMT 函数封装能力清单](docs/QMT函数封装能力清单.md)。
|
|
321
|
+
|
|
322
|
+
账号订阅成功后,SDK 会在后台约每两秒重新确认订阅。用户程序保持运行时,QMT、cfquant Web 或通信服务重启后,待桥接策略恢复授权、通道可用,SDK 会自动恢复该账号的新回调;默认 Web 路由和直接 Pipe/LTtx 连接均适用。主动 `unsubscribe(account)` 后不会再恢复该账号,`stop()` / `disconnect()` 会停止后台恢复。此机制只恢复账号订阅,不补发停机期间的回调,也不重试委托或撤单。升级 SDK 后需先重启一次用户程序加载新代码,此后无需随 QMT 重启用户程序。
|
|
323
|
+
|
|
324
|
+
推荐迁移顺序:**行情查询 → 资金/持仓 → 回调 → 模拟委托 → 模拟撤单 → 正式策略**。每一步都先在 Web 控制台接口测试和日志中确认,再进入下一步。
|
|
323
325
|
|
|
324
326
|
## Web 控制台
|
|
325
327
|
|
|
@@ -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
|
|
|
@@ -197,6 +197,8 @@ def batch_cancel_result_rows(cancels, seqs=None):
|
|
|
197
197
|
def batch_result(account, batch_id, asynchronous, results, operation="order"):
|
|
198
198
|
counts = {name: sum(row["status"] == name for row in results)
|
|
199
199
|
for name in ("submitted", "failed", "unknown", "skipped")}
|
|
200
|
+
counts["rejected"] = sum(row.get("dispatched") is False for row in results)
|
|
201
|
+
counts["retryable"] = sum(row.get("safe_to_retry") is True for row in results)
|
|
200
202
|
return dict(counts, batch_id=batch_id, account=account, asynchronous=asynchronous,
|
|
201
203
|
operation=operation, execution="qmt", total=len(results), attempted=len(results) - counts["skipped"],
|
|
202
204
|
ok=counts["submitted"] == len(results), results=results)
|
|
@@ -207,6 +209,19 @@ def batch_unknown(rows, error):
|
|
|
207
209
|
row.update(status="unknown", ok=None, error=str(error) or type(error).__name__)
|
|
208
210
|
|
|
209
211
|
|
|
212
|
+
def batch_rejected(rows, error):
|
|
213
|
+
"""Mark a rejection known to happen before QMT dispatch."""
|
|
214
|
+
details = getattr(error, "error_details", None)
|
|
215
|
+
details = details if isinstance(details, dict) else {}
|
|
216
|
+
message = str(error) or type(error).__name__
|
|
217
|
+
for row in rows:
|
|
218
|
+
row.update(status="failed", ok=False, error=message, dispatched=False,
|
|
219
|
+
safe_to_retry=details.get("safe_to_retry") is True,
|
|
220
|
+
retry_when=details.get("retry_when", ""),
|
|
221
|
+
error_code=details.get("code", ""),
|
|
222
|
+
error_stage=details.get("stage", "before_dispatch"))
|
|
223
|
+
|
|
224
|
+
|
|
210
225
|
def validate_batch_response(result, params, asynchronous):
|
|
211
226
|
expected = params["orders"]
|
|
212
227
|
if (not isinstance(result, dict) or result.get("execution") != "qmt"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Order-only extensions sharing an existing XtQuantTrader session and callbacks."""
|
|
2
2
|
|
|
3
3
|
from .batch_orders import (
|
|
4
|
-
batch_cancel_result_rows, batch_result, batch_result_rows, batch_unknown,
|
|
4
|
+
batch_cancel_result_rows, batch_rejected, batch_result, batch_result_rows, batch_unknown,
|
|
5
5
|
prepare_batch_cancels, prepare_batch_orders, validate_batch_cancel_response, validate_batch_response,
|
|
6
6
|
)
|
|
7
7
|
from .protocol import new_id
|
|
@@ -86,9 +86,18 @@ class CfQuantTrader:
|
|
|
86
86
|
result = validate_batch_response(response, params, asynchronous)
|
|
87
87
|
except Exception as error:
|
|
88
88
|
results = batch_result_rows(rows, seqs)
|
|
89
|
-
|
|
89
|
+
if getattr(error, "dispatched", None) is False:
|
|
90
|
+
batch_rejected(results, error)
|
|
91
|
+
else:
|
|
92
|
+
batch_unknown(results, "Batch response unavailable; QMT may still execute the batch: %s" % error)
|
|
90
93
|
result = batch_result(account, batch_id, asynchronous, results)
|
|
91
94
|
result["request_error"] = str(error) or type(error).__name__
|
|
95
|
+
details = getattr(error, "error_details", None)
|
|
96
|
+
if isinstance(details, dict):
|
|
97
|
+
result.update(error_code=details.get("code", ""), error_stage=details.get("stage", ""),
|
|
98
|
+
dispatched=details.get("dispatched"), safe_to_retry=details.get("safe_to_retry") is True,
|
|
99
|
+
retry_when=details.get("retry_when", ""),
|
|
100
|
+
runtime_instance_id=details.get("runtime_instance_id", ""))
|
|
92
101
|
if asynchronous:
|
|
93
102
|
for row in result["results"]:
|
|
94
103
|
if row["status"] in ("failed", "skipped"):
|
|
@@ -113,7 +122,16 @@ class CfQuantTrader:
|
|
|
113
122
|
return validate_batch_cancel_response(response, params, asynchronous)
|
|
114
123
|
except Exception as error:
|
|
115
124
|
results = batch_cancel_result_rows(rows, seqs)
|
|
116
|
-
|
|
125
|
+
if getattr(error, "dispatched", None) is False:
|
|
126
|
+
batch_rejected(results, error)
|
|
127
|
+
else:
|
|
128
|
+
batch_unknown(results, "Batch cancel response unavailable; QMT may still execute the batch: %s" % error)
|
|
117
129
|
result = batch_result(account, batch_id, asynchronous, results, operation="cancel")
|
|
118
130
|
result["request_error"] = str(error) or type(error).__name__
|
|
131
|
+
details = getattr(error, "error_details", None)
|
|
132
|
+
if isinstance(details, dict):
|
|
133
|
+
result.update(error_code=details.get("code", ""), error_stage=details.get("stage", ""),
|
|
134
|
+
dispatched=details.get("dispatched"), safe_to_retry=details.get("safe_to_retry") is True,
|
|
135
|
+
retry_when=details.get("retry_when", ""),
|
|
136
|
+
runtime_instance_id=details.get("runtime_instance_id", ""))
|
|
119
137
|
return result
|
|
@@ -12,8 +12,17 @@ from .protocol import decode_value, dumps_message, loads_message, new_id, pack_r
|
|
|
12
12
|
from .rpc_timing import RpcTiming
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
class CfquantError(RuntimeError):
|
|
16
|
-
|
|
15
|
+
class CfquantError(RuntimeError):
|
|
16
|
+
"""RPC failure retaining the server's structured rejection details."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, message="", details=None):
|
|
19
|
+
super(CfquantError, self).__init__(message)
|
|
20
|
+
self.error_details = dict(details) if isinstance(details, dict) else {}
|
|
21
|
+
self.code = self.error_details.get("code", "")
|
|
22
|
+
self.stage = self.error_details.get("stage", "")
|
|
23
|
+
self.dispatched = self.error_details.get("dispatched")
|
|
24
|
+
self.safe_to_retry = self.error_details.get("safe_to_retry", False) is True
|
|
25
|
+
self.retry_when = self.error_details.get("retry_when", "")
|
|
17
26
|
|
|
18
27
|
|
|
19
28
|
class CfquantTimeout(TimeoutError):
|
|
@@ -209,6 +218,7 @@ class LTtxRpcClient(object):
|
|
|
209
218
|
with self._pending_lock:
|
|
210
219
|
self._pending[request_id] = q
|
|
211
220
|
self._push("request", raw, request_channel or self.request_channel)
|
|
221
|
+
timing.sent()
|
|
212
222
|
try:
|
|
213
223
|
msg = _wait_response(q, effective_timeout)
|
|
214
224
|
except queue.Empty:
|
|
@@ -216,7 +226,7 @@ class LTtxRpcClient(object):
|
|
|
216
226
|
timing.received(msg)
|
|
217
227
|
if not msg.get("ok"):
|
|
218
228
|
err = msg.get("error") or {}
|
|
219
|
-
raise CfquantError(err.get("message") or str(err))
|
|
229
|
+
raise CfquantError(err.get("message") or str(err), details=err)
|
|
220
230
|
return timing.decode(msg.get("result"), decode_value)
|
|
221
231
|
except (KeyboardInterrupt, SystemExit):
|
|
222
232
|
self.close()
|
|
@@ -837,6 +837,7 @@ class NormalQmtBridge(TxTradeBridge):
|
|
|
837
837
|
if not active:
|
|
838
838
|
return
|
|
839
839
|
msg = active[0][0]
|
|
840
|
+
dispatch_started = time.perf_counter()
|
|
840
841
|
result = self._dispatch(msg.get("action"), msg.get("params") or {}, msg)
|
|
841
842
|
with self.coalesce_lock:
|
|
842
843
|
entry = self.coalesced_requests.pop(coalesce_key, None)
|
|
@@ -847,6 +848,15 @@ class NormalQmtBridge(TxTradeBridge):
|
|
|
847
848
|
encoded_result = encode_response_result(result)
|
|
848
849
|
while response_waiters:
|
|
849
850
|
waiter_msg, _ = response_waiters[0]
|
|
851
|
+
if msg.get("_response_meta") and str(msg.get("action", "")).startswith("xttrader.query_stock_"):
|
|
852
|
+
# Coalesced readers share execution, but each keeps its own ID
|
|
853
|
+
# and queue delay (including readers arriving during execution).
|
|
854
|
+
timing = dict(msg["_response_meta"]["timing"]["qmt"])
|
|
855
|
+
received = waiter_msg.get("_received_monotonic")
|
|
856
|
+
if received is not None:
|
|
857
|
+
timing["queue_ms"] = round(max(0.0, dispatch_started - received) * 1000, 3)
|
|
858
|
+
timing["coalesced_request_id"] = msg.get("id")
|
|
859
|
+
waiter_msg["_response_meta"] = {"timing": {"qmt": timing}}
|
|
850
860
|
self._send_response(waiter_msg, result, encoded_result=encoded_result)
|
|
851
861
|
response_waiters.pop(0)
|
|
852
862
|
self._log(
|
|
@@ -864,6 +874,13 @@ class NormalQmtBridge(TxTradeBridge):
|
|
|
864
874
|
% (source, msg.get("action"), msg.get("id"), len(waiters), e)
|
|
865
875
|
)
|
|
866
876
|
for waiter_msg, _ in waiters:
|
|
877
|
+
if msg.get("_response_meta") and str(msg.get("action", "")).startswith("xttrader.query_stock_"):
|
|
878
|
+
timing = dict(msg["_response_meta"]["timing"]["qmt"])
|
|
879
|
+
received = waiter_msg.get("_received_monotonic")
|
|
880
|
+
if received is not None:
|
|
881
|
+
timing["queue_ms"] = round(max(0.0, dispatch_started - received) * 1000, 3)
|
|
882
|
+
timing["coalesced_request_id"] = msg.get("id")
|
|
883
|
+
waiter_msg["_response_meta"] = {"timing": {"qmt": timing}}
|
|
867
884
|
self._send_error(waiter_msg, e)
|
|
868
885
|
|
|
869
886
|
def _on_whole_quote(self, data):
|
|
@@ -908,6 +925,13 @@ class NormalQmtBridge(TxTradeBridge):
|
|
|
908
925
|
client_id = msg.get("client_id") or msg.get("reply_channel")
|
|
909
926
|
if not client_id:
|
|
910
927
|
return
|
|
928
|
+
if (not msg.get("_response_meta") and str(msg.get("action", "")).startswith("xttrader.query_stock_")
|
|
929
|
+
and str(error) == "read-only request expired before QMT execution"):
|
|
930
|
+
received = msg.get("_received_monotonic")
|
|
931
|
+
msg["_response_meta"] = {"timing": {"qmt": {
|
|
932
|
+
"available": True, "executed": False, "native_ms": 0.0, "execute_ms": 0.0,
|
|
933
|
+
"queue_ms": round((time.perf_counter() - received) * 1000, 3) if received is not None else None,
|
|
934
|
+
}}}
|
|
911
935
|
self._log(
|
|
912
936
|
"normal bridge send_error action=%s id=%s client_id=%s error=%s"
|
|
913
937
|
% (msg.get("action"), msg.get("id"), client_id, error)
|
|
@@ -48,18 +48,24 @@ class PipeRpcClient(object):
|
|
|
48
48
|
if self._started:
|
|
49
49
|
self._started = False
|
|
50
50
|
self._close_conns_locked()
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
51
|
+
try:
|
|
52
|
+
self._rx_conn = connect_pipe(self.pipe_name, timeout_ms=self.connect_timeout_ms)
|
|
53
|
+
self._rx_conn.write_frame(dumps_pipe_message({
|
|
54
|
+
"type": "hello",
|
|
55
|
+
"role": "api_rx",
|
|
56
|
+
"client_id": self.client_id,
|
|
57
|
+
}))
|
|
58
|
+
self._tx_conn = connect_pipe(self.pipe_name, timeout_ms=self.connect_timeout_ms)
|
|
59
|
+
self._tx_conn.write_frame(dumps_pipe_message({
|
|
60
|
+
"type": "hello",
|
|
61
|
+
"role": "api_tx",
|
|
62
|
+
"client_id": self.client_id,
|
|
63
|
+
}))
|
|
64
|
+
except BaseException:
|
|
65
|
+
# A retry must not orphan the receive pipe when opening or
|
|
66
|
+
# registering the send pipe failed halfway through startup.
|
|
67
|
+
self.close()
|
|
68
|
+
raise
|
|
63
69
|
self._started = True
|
|
64
70
|
self._closed = False
|
|
65
71
|
generation = self._event_dispatcher.start()
|
|
@@ -95,6 +101,7 @@ class PipeRpcClient(object):
|
|
|
95
101
|
self._pending[request_id] = q
|
|
96
102
|
try:
|
|
97
103
|
self._send_request(raw, request_channel or self.request_channel)
|
|
104
|
+
timing.sent()
|
|
98
105
|
except Exception:
|
|
99
106
|
self.close()
|
|
100
107
|
raise
|
|
@@ -111,7 +118,7 @@ class PipeRpcClient(object):
|
|
|
111
118
|
err = msg.get("error") or {}
|
|
112
119
|
from .client import CfquantError
|
|
113
120
|
|
|
114
|
-
raise CfquantError(err.get("message") or str(err))
|
|
121
|
+
raise CfquantError(err.get("message") or str(err), details=err)
|
|
115
122
|
return timing.decode(msg.get("result"), decode_value)
|
|
116
123
|
except (KeyboardInterrupt, SystemExit):
|
|
117
124
|
self.close()
|
|
@@ -215,10 +215,11 @@ def encode_error(error):
|
|
|
215
215
|
return None
|
|
216
216
|
if isinstance(error, dict):
|
|
217
217
|
return error
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
218
|
+
details = getattr(error, "error_details", None)
|
|
219
|
+
result = dict(details) if isinstance(details, dict) else {}
|
|
220
|
+
result.setdefault("type", type(error).__name__)
|
|
221
|
+
result["message"] = str(error)
|
|
222
|
+
return result
|
|
222
223
|
|
|
223
224
|
|
|
224
225
|
def encode_value(value):
|
|
@@ -18,9 +18,12 @@ class QmtNotReady(RuntimeError):
|
|
|
18
18
|
|
|
19
19
|
def verify_environment(directory):
|
|
20
20
|
"""Validate dependencies and native ABI before stopping a running terminal."""
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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"]
|
|
24
27
|
directory = Path(directory)
|
|
25
28
|
for module in ("Qt5Core.dll", "Qt5Widgets.dll"):
|
|
26
29
|
version = win32api.GetFileVersionInfo(str(directory / module), "\\")
|
|
@@ -83,12 +86,16 @@ def select_binding(config_path, name):
|
|
|
83
86
|
|
|
84
87
|
class QtLoginBackend:
|
|
85
88
|
def __init__(self, directory, timeout=40, expected_pid=None):
|
|
86
|
-
|
|
89
|
+
try:
|
|
90
|
+
from .qmt_dependencies import import_login_module
|
|
91
|
+
except ImportError:
|
|
92
|
+
from qmt_dependencies import import_login_module
|
|
87
93
|
import psutil
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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")
|
|
92
99
|
self.gui, self.process = win32gui, win32process
|
|
93
100
|
self.timeout = timeout
|
|
94
101
|
self.hwnd = None
|
|
@@ -232,8 +239,12 @@ class QtLoginBackend:
|
|
|
232
239
|
self.failure_hwnd = None
|
|
233
240
|
|
|
234
241
|
def close(self):
|
|
235
|
-
|
|
236
|
-
|
|
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")
|
|
237
248
|
try:
|
|
238
249
|
if self.session:
|
|
239
250
|
self.session.detach()
|
|
@@ -6,6 +6,8 @@ import os
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
import threading
|
|
8
8
|
|
|
9
|
+
from .qmt_dependencies import QmtDependencyError, import_login_module
|
|
10
|
+
|
|
9
11
|
_LOCK = threading.RLock()
|
|
10
12
|
|
|
11
13
|
|
|
@@ -17,12 +19,12 @@ def directory_key(directory):
|
|
|
17
19
|
|
|
18
20
|
|
|
19
21
|
def _protect(value, entropy):
|
|
20
|
-
|
|
22
|
+
win32crypt = import_login_module("win32crypt")
|
|
21
23
|
return win32crypt.CryptProtectData(value, "CFQuant QMT login", entropy, None, None, 1)
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
def _unprotect(value, entropy):
|
|
25
|
-
|
|
27
|
+
win32crypt = import_login_module("win32crypt")
|
|
26
28
|
return win32crypt.CryptUnprotectData(value, entropy, None, None, 1)[1]
|
|
27
29
|
|
|
28
30
|
|
|
@@ -50,6 +52,8 @@ class QmtCredentialStore:
|
|
|
50
52
|
key = directory_key(directory)
|
|
51
53
|
try:
|
|
52
54
|
encrypted = _protect(password.encode("utf-8"), key.encode("ascii"))
|
|
55
|
+
except QmtDependencyError:
|
|
56
|
+
raise
|
|
53
57
|
except Exception:
|
|
54
58
|
raise RuntimeError("无法使用当前 Windows 用户加密 QMT 密码,请检查 pywin32 安装") from None
|
|
55
59
|
with _LOCK:
|
|
@@ -68,5 +72,7 @@ class QmtCredentialStore:
|
|
|
68
72
|
return ""
|
|
69
73
|
try:
|
|
70
74
|
return _unprotect(base64.b64decode(entry["secret"]), key.encode("ascii")).decode("utf-8")
|
|
75
|
+
except QmtDependencyError:
|
|
76
|
+
raise
|
|
71
77
|
except Exception:
|
|
72
78
|
raise RuntimeError("无法解密 QMT 密码,请使用保存密码时的 Windows 用户运行,或重新保存密码") from None
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Read-only checks and repair instructions for optional QMT login components."""
|
|
2
|
+
import copy
|
|
3
|
+
import importlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
LOGIN_MODULES = ("frida", "win32api", "win32crypt", "win32event", "win32gui", "win32process")
|
|
13
|
+
_LOCK = threading.Lock()
|
|
14
|
+
_CACHE = {}
|
|
15
|
+
_PROBE_CODE = r'''
|
|
16
|
+
import importlib, importlib.metadata, json, os, pathlib, sys
|
|
17
|
+
checks = []
|
|
18
|
+
for name in MODULES:
|
|
19
|
+
try:
|
|
20
|
+
module = importlib.import_module(name)
|
|
21
|
+
checks.append(dict(name=name, ok=True, path=getattr(module, '__file__', '')))
|
|
22
|
+
except Exception as error:
|
|
23
|
+
checks.append(dict(name=name, ok=False, error=str(error), error_type=type(error).__name__))
|
|
24
|
+
packages = {}
|
|
25
|
+
for name in ('pywin32', 'frida'):
|
|
26
|
+
try:
|
|
27
|
+
packages[name] = importlib.metadata.version(name)
|
|
28
|
+
except importlib.metadata.PackageNotFoundError:
|
|
29
|
+
packages[name] = ''
|
|
30
|
+
prefix = pathlib.Path(sys.prefix)
|
|
31
|
+
base_prefix = pathlib.Path(getattr(sys, 'base_prefix', sys.prefix))
|
|
32
|
+
conda = (prefix / 'conda-meta').is_dir()
|
|
33
|
+
venv = not conda and prefix != base_prefix
|
|
34
|
+
environment = 'conda' if conda else 'venv' if venv else 'python'
|
|
35
|
+
print(json.dumps(dict(python_executable=sys.executable, python_version=sys.version.split()[0],
|
|
36
|
+
prefix=str(prefix), base_prefix=str(base_prefix), environment=environment,
|
|
37
|
+
global_python=sys.prefix == sys.base_prefix,
|
|
38
|
+
conda_executable=os.environ.get('CONDA_EXE', '') if conda else '',
|
|
39
|
+
conda_pywin32=conda and bool(list((prefix / 'conda-meta').glob('pywin32-*.json'))),
|
|
40
|
+
supported=sys.platform == 'win32', checks=checks, packages=packages), ensure_ascii=True))
|
|
41
|
+
'''
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _ps_quote(value):
|
|
45
|
+
return "'" + str(value).replace("'", "''") + "'"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _finish_report(report):
|
|
49
|
+
report["checked_at"] = time.time()
|
|
50
|
+
report["ok"] = bool(report.get("supported") and not report.get("error")
|
|
51
|
+
and all(item["ok"] for item in report.get("checks", [])))
|
|
52
|
+
failed = [item for item in report.get("checks", []) if not item["ok"]]
|
|
53
|
+
if not report.get("supported") and not report.get("error"):
|
|
54
|
+
report["message"] = "当前系统不支持 QMT 组件自动登录,请手动启动并登录 QMT。"
|
|
55
|
+
elif report["ok"]:
|
|
56
|
+
report["message"] = "QMT 自动登录组件检测通过。"
|
|
57
|
+
else:
|
|
58
|
+
names = "、".join(item["name"] for item in failed) or "Python 环境"
|
|
59
|
+
report["message"] = "QMT 自动登录组件不可用(%s)。可取消自动启动,保存配置后手动启动并登录 QMT。" % names
|
|
60
|
+
commands = []
|
|
61
|
+
python = "& " + _ps_quote(report["python_executable"])
|
|
62
|
+
environment = str(report.get("environment") or "").lower()
|
|
63
|
+
if environment not in {"conda", "venv", "python"}:
|
|
64
|
+
environment = "python" if report.get("global_python") else "venv"
|
|
65
|
+
if report.get("supported") and failed:
|
|
66
|
+
if any(item["name"].startswith("win32") for item in failed):
|
|
67
|
+
if environment == "conda" and report.get("conda_pywin32"):
|
|
68
|
+
conda = report.get("conda_executable") or "conda"
|
|
69
|
+
commands.append("& %s install --prefix %s --force-reinstall 'pywin32>=306'" % (
|
|
70
|
+
_ps_quote(conda), _ps_quote(report["prefix"])))
|
|
71
|
+
else:
|
|
72
|
+
commands.append(python + " -m pip install --no-cache-dir --upgrade --force-reinstall 'pywin32>=306'")
|
|
73
|
+
if environment == "python":
|
|
74
|
+
commands.append(python + " -m pywin32_postinstall -install")
|
|
75
|
+
if any(item["name"] == "frida" for item in failed):
|
|
76
|
+
commands.append(python + " -m pip install --no-cache-dir --upgrade --force-reinstall 'frida>=17.0,<18'")
|
|
77
|
+
commands.append(python + " -c " + _ps_quote("import " + ", ".join(LOGIN_MODULES) + "; print('QMT login components OK')"))
|
|
78
|
+
report["repair_commands"] = commands
|
|
79
|
+
if environment == "conda":
|
|
80
|
+
report["repair_note"] = (
|
|
81
|
+
"已检测到 Conda 环境。先停止 CFQuant,在已激活该环境的 Anaconda Prompt 或 PowerShell 中执行;"
|
|
82
|
+
"命令会绑定当前环境的前缀。验证通过后重启 CFQuant,再重新检测。"
|
|
83
|
+
)
|
|
84
|
+
elif environment == "venv":
|
|
85
|
+
report["repair_note"] = (
|
|
86
|
+
"已检测到 venv 虚拟环境。先停止 CFQuant,在普通 PowerShell 中执行下面的命令;"
|
|
87
|
+
"命令已绑定当前 venv 的 Python,不要切换到系统 Python,也不要运行 pywin32_postinstall。"
|
|
88
|
+
"验证通过后重启 CFQuant,再重新检测。"
|
|
89
|
+
)
|
|
90
|
+
else:
|
|
91
|
+
report["repair_note"] = (
|
|
92
|
+
"已检测到独立 Python 环境。先停止 CFQuant,在 PowerShell 中执行下面的命令;"
|
|
93
|
+
"命令已绑定当前 Python,安装完成后会执行 pywin32_postinstall。"
|
|
94
|
+
"验证通过后重启 CFQuant,再重新检测。"
|
|
95
|
+
)
|
|
96
|
+
return report
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def check_login_dependencies(python_exe=None, refresh=False, timeout=15):
|
|
100
|
+
"""Probe the actual interpreter without installing packages or loading DLLs here."""
|
|
101
|
+
executable = str(python_exe or sys.executable)
|
|
102
|
+
with _LOCK:
|
|
103
|
+
if not refresh and executable in _CACHE:
|
|
104
|
+
return copy.deepcopy(_CACHE[executable])
|
|
105
|
+
report = {"python_executable": executable, "supported": sys.platform == "win32", "checks": []}
|
|
106
|
+
if not report["supported"]:
|
|
107
|
+
_CACHE[executable] = _finish_report(report)
|
|
108
|
+
return copy.deepcopy(_CACHE[executable])
|
|
109
|
+
try:
|
|
110
|
+
run_kwargs = {}
|
|
111
|
+
if os.name == "nt":
|
|
112
|
+
run_kwargs["creationflags"] = getattr(subprocess, "CREATE_NO_WINDOW", 0)
|
|
113
|
+
completed = subprocess.run(
|
|
114
|
+
[executable, "-c", "MODULES = %r\n" % (LOGIN_MODULES,) + _PROBE_CODE],
|
|
115
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding="utf-8",
|
|
116
|
+
errors="replace", timeout=timeout,
|
|
117
|
+
**run_kwargs,
|
|
118
|
+
)
|
|
119
|
+
if completed.returncode:
|
|
120
|
+
raise RuntimeError((completed.stderr or completed.stdout or "Python dependency probe failed")[-4000:])
|
|
121
|
+
# Startup hooks may print before our JSON report.
|
|
122
|
+
report.update(json.loads(completed.stdout.strip().splitlines()[-1]))
|
|
123
|
+
except Exception as error:
|
|
124
|
+
report["error"] = str(error)
|
|
125
|
+
_CACHE[executable] = _finish_report(report)
|
|
126
|
+
return copy.deepcopy(_CACHE[executable])
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class QmtDependencyError(RuntimeError):
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def import_login_module(name):
|
|
134
|
+
"""Keep in-process import errors actionable, including stale loaded DLLs."""
|
|
135
|
+
try:
|
|
136
|
+
return importlib.import_module(name)
|
|
137
|
+
except (ImportError, OSError) as error:
|
|
138
|
+
raise QmtDependencyError(
|
|
139
|
+
"QMT 自动登录组件 %s 无法加载;运行环境:%s;原因:%s。"
|
|
140
|
+
"请查看自动登录组件检测中的修复说明,或取消自动启动后手动登录 QMT。"
|
|
141
|
+
% (name, sys.executable, error)
|
|
142
|
+
) from error
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def report_startup_dependencies(python_exe=None):
|
|
146
|
+
report = check_login_dependencies(python_exe, refresh=True)
|
|
147
|
+
if not report.get("supported"):
|
|
148
|
+
return report
|
|
149
|
+
level = "INFO" if report["ok"] else "WARN"
|
|
150
|
+
print("[%s] %s Python=%s" % (level, report["message"], report["python_executable"]))
|
|
151
|
+
for item in report["checks"]:
|
|
152
|
+
if not item["ok"]:
|
|
153
|
+
print("[WARN] %s: %s" % (item["name"], item["error"]))
|
|
154
|
+
if report.get("error"):
|
|
155
|
+
print("[WARN] %s" % report["error"])
|
|
156
|
+
for command in report["repair_commands"]:
|
|
157
|
+
print("[REPAIR] %s" % command)
|
|
158
|
+
return report
|
|
@@ -5,11 +5,23 @@ import os as _cq_os
|
|
|
5
5
|
import sys as _cq_sys
|
|
6
6
|
import threading as _cq_threading
|
|
7
7
|
import time as _cq_time
|
|
8
|
+
import uuid as _cq_uuid
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ManagedStrategyError(RuntimeError):
|
|
12
|
+
"""A rejection made before entering any QMT business dispatcher."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, message, code, runtime_instance_id, retry_when=""):
|
|
15
|
+
super(ManagedStrategyError, self).__init__(message)
|
|
16
|
+
self.error_details = dict(code=code, stage="before_dispatch", dispatched=False,
|
|
17
|
+
safe_to_retry=bool(retry_when), retry_when=retry_when,
|
|
18
|
+
runtime_instance_id=runtime_instance_id)
|
|
8
19
|
|
|
9
20
|
|
|
10
21
|
class ManagedPipeRuntime(object):
|
|
11
22
|
def __init__(self, descriptor=None):
|
|
12
23
|
self.descriptor = dict(descriptor or {})
|
|
24
|
+
self.runtime_instance_id = _cq_uuid.uuid4().hex
|
|
13
25
|
self.active = False
|
|
14
26
|
self.approved = False
|
|
15
27
|
self.revoked = False
|
|
@@ -49,7 +61,8 @@ class ManagedPipeRuntime(object):
|
|
|
49
61
|
state = "error" if self.failure else ("stopped" if not self.active or self.revoked
|
|
50
62
|
else ("running" if ready and self.approved else "loaded"))
|
|
51
63
|
return dict(self.descriptor, state=state, error=self.failure,
|
|
52
|
-
pid=_cq_os.getpid(), updated_at=_cq_time.time()
|
|
64
|
+
pid=_cq_os.getpid(), updated_at=_cq_time.time(),
|
|
65
|
+
runtime_instance_id=self.runtime_instance_id)
|
|
53
66
|
|
|
54
67
|
def control(self, desired):
|
|
55
68
|
if desired.get("runtime_key") != self.descriptor.get("runtime_key"):
|
|
@@ -78,9 +91,18 @@ class ManagedPipeRuntime(object):
|
|
|
78
91
|
return self.control(params or {})
|
|
79
92
|
if action == "cfquant.status":
|
|
80
93
|
return function(action, params, msg)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
94
|
+
with self.mutex:
|
|
95
|
+
if not self.active or self.revoked:
|
|
96
|
+
raise ManagedStrategyError("The account's cfquant strategy mode has changed.",
|
|
97
|
+
"STRATEGY_MODE_CHANGED", self.runtime_instance_id)
|
|
98
|
+
if not self.approved:
|
|
99
|
+
raise ManagedStrategyError(
|
|
100
|
+
"Managed strategy is awaiting account mode confirmation from cfquant Web.",
|
|
101
|
+
"ACCOUNT_MODE_UNCONFIRMED", self.runtime_instance_id, "account_ready")
|
|
102
|
+
result = dispatch_request(action, params, msg)
|
|
103
|
+
if action == "cfquant.ping" and isinstance(result, dict):
|
|
104
|
+
result = dict(result, managed_runtime=self.status())
|
|
105
|
+
return result
|
|
84
106
|
return guarded
|
|
85
107
|
|
|
86
108
|
def wrap(self, function, reject=False):
|
|
@@ -89,7 +111,8 @@ class ManagedPipeRuntime(object):
|
|
|
89
111
|
with self.mutex:
|
|
90
112
|
if not self.active or self.revoked:
|
|
91
113
|
if reject:
|
|
92
|
-
raise
|
|
114
|
+
raise ManagedStrategyError("The account's cfquant strategy mode has changed.",
|
|
115
|
+
"STRATEGY_MODE_CHANGED", self.runtime_instance_id)
|
|
93
116
|
return None
|
|
94
117
|
self.calls += 1
|
|
95
118
|
try:
|