cfquant 0.2.59__tar.gz → 0.2.61__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cfquant-0.2.59 → cfquant-0.2.61}/PKG-INFO +2 -2
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/_editable_install.py +30 -4
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/normal_bridge.py +1 -1
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/pipe_bridge.py +37 -17
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/pipe_transport.py +102 -36
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/protocol.py +2 -1
- cfquant-0.2.61/cfquant/qmt_binding_defaults.py +54 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/qmt_component_login.py +14 -2
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/qmt_dependencies.py +35 -3
- cfquant-0.2.61/cfquant/qmt_native_worker.py +253 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/qmt_pipe_runtime.py +42 -16
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/qmt_strategy_recovery.py +7 -2
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/rpc_timing.py +2 -0
- cfquant-0.2.61/cfquant/tests/test_account_subscription_probe.py +233 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_lite_account_routing.py +4 -2
- cfquant-0.2.61/cfquant/tests/test_pipe_shutdown.py +394 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_qmt_auto_login_browser.py +31 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_qmt_auto_login_integration.py +30 -6
- cfquant-0.2.61/cfquant/tests/test_qmt_binding_defaults.py +116 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_qmt_component_login.py +6 -6
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_qmt_dependencies.py +68 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_qmt_fileless.py +15 -1
- cfquant-0.2.61/cfquant/tests/test_qmt_native_worker.py +166 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_qmt_strategy_browser.py +1 -1
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_qmt_strategy_recovery.py +20 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_qmt_strategy_web.py +3 -3
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_subscription_congestion.py +7 -6
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_trader_subscription_recovery.py +21 -6
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tx_trade_bridge.py +33 -2
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/version.py +2 -2
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/xttrader.py +43 -7
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant.egg-info/PKG-INFO +2 -2
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant.egg-info/SOURCES.txt +6 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant.egg-info/requires.txt +1 -1
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant_web_server.py +92 -14
- {cfquant-0.2.59 → cfquant-0.2.61}/pyproject.toml +2 -2
- {cfquant-0.2.59 → cfquant-0.2.61}/qmt_scripts/CFQUANT_LITE.py +262 -118
- {cfquant-0.2.59 → cfquant-0.2.61}/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 +262 -118
- {cfquant-0.2.59 → cfquant-0.2.61}/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 +262 -118
- {cfquant-0.2.59 → cfquant-0.2.61}/requirements.txt +1 -1
- {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/app.js +5 -5
- {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/index.html +8 -8
- {cfquant-0.2.59 → cfquant-0.2.61}/LICENSE +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/LTtx/__init__.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/LTtx/tx/Config.txt +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/LTtx/tx/LTtx_server.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/LTtx/tx/__init__.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/LTtx/tx/tx.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/MANIFEST.in +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/README.md +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/__init__.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/account_routing.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/algo.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/algo_broker.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/batch_orders.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/build_info.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/callback_dispatcher.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/cftrader.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/channels.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/cli.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/client.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/config.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/diagnostics.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/flow_control.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/hub.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/level2.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/log_management.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/logging_i18n.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/management.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/order_identity.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/order_meta.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/pipe_client.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/pipe_hub.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/qmt_binding_settings.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/qmt_bridge.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/qmt_component_agent.js +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/qmt_credentials.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/qmt_strategy_agent.js +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/qmt_strategy_deploy.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/qmt_strategy_package.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/qmt_strategy_runtime.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/qmt_update.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/quote_history.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/quote_leases.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/quote_service.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/remote_adapter.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/remote_management.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/remote_supervisor.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/runtime_report.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/shared_quotes.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/stock_connect.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/10_/345/221/275/344/273/244/350/241/214/346/216/245/345/217/243/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/11_/346/231/256/351/200/232/346/241/245/346/216/245/346/264/276/345/217/221/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/12_/350/256/242/345/215/225/345/244/207/346/263/250/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/13_/347/256/241/351/201/223/344/270/255/345/277/203/350/277/236/346/216/245/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/14_/350/277/220/350/241/214/346/227/266/346/212/245/345/221/212/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/15_/347/275/221/351/241/265/347/212/266/346/200/201/347/274/223/345/255/230/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/16_/345/220/257/345/212/250/345/256/211/350/243/205/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/17_/345/244/247QMT/346/216/245/345/217/243/351/200/202/351/205/215/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/18_/351/253/230/347/272/247/346/250/241/345/274/217/345/256/236/346/234/272/350/201/224/350/260/203.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/19_/345/256/236/346/234/272/350/201/224/350/260/203/345/210/244/345/256/232/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/1_/350/241/214/346/203/205/346/216/245/346/224/266/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/20_/350/201/224/350/260/203/351/227/256/351/242/230/344/277/256/345/244/215/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/21_/345/215/225/350/202/241/350/241/214/346/203/205/350/256/242/351/230/205/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/22_/350/277/236/346/216/245/350/257/212/346/226/255/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/23_/347/244/272/344/276/213/351/205/215/347/275/256/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/24_/350/264/246/346/210/267/347/274/223/345/255/230/346/214/211/351/234/200/345/210/267/346/226/260/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/25_/350/264/246/346/210/267/347/274/223/345/255/230/351/241/265/351/235/242/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/26_/345/205/250/346/216/250/350/256/242/351/230/205/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/27_/345/205/250/346/216/250/350/256/242/351/230/205/351/241/265/351/235/242/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/28_Level2/346/216/245/345/217/243/351/200/202/351/205/215/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/29_Level2/350/241/214/346/203/205/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/2_/346/225/260/346/215/256/350/216/267/345/217/226/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/30_cftrader/346/211/271/351/207/217/344/270/213/345/215/225/346/200/247/350/203/275/345/237/272/345/207/206.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/31_cftrader/346/250/241/346/213/237/350/264/246/345/217/267/346/211/271/351/207/217/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/32_/345/274/202/346/255/245/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/33_/345/220/214/346/255/245/344/270/213/345/215/225/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/34_/345/205/250/346/216/250/345/206/205/351/203/250/345/273/266/350/277/237/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/3_/346/225/260/346/215/256/344/270/213/350/275/275/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/4_/344/272/244/346/230/223/345/247/224/346/211/230/346/237/245/350/257/242/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/5_/347/234/237/345/256/236/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/6_/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/7_/345/220/214/346/255/245/345/274/202/346/255/245/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/8_/346/210/220/344/272/244/350/256/242/345/215/225/345/222/214/345/247/224/346/211/230/350/256/242/345/215/225/346/237/245/350/257/242/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/9_/345/217/257/346/222/244/350/256/242/345/215/225/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/_helpers.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/readme.md +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_account_query_timing.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_advanced_parity.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_algo.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_algo_broker.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_api_tester_browser.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_api_tester_web.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_binding_live_refresh_browser.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_binding_runtime_browser.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_callback_recovery.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_callback_requests.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_cftrader.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_cftrader_tutorial.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_cftrader_web.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_credit_compat.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_dataframe_encoding.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_json_serialization.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_latency_flow_control.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_lite_deployment_paths.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_lite_parity.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_lite_qmt_dispatch.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_local_trading_calendar.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_log_management.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_management.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_management_batch.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_market_data_fallback.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_market_timing.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_order_identity.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_order_time_callback.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_project_updater.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_python_api_reference.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_qmt_binding_settings.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_qmt_strategy_deploy.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_qmt_update.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_quote_latency.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_quote_latency_encoding.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_quote_service.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_remote_management.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_remote_management_browser.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_remote_version_check.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_shared_quotes.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_shared_quotes_generated.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_startup_recovery.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_stock_connect.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_stock_connect_browser.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_table_conversion.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_trade_strategy_callback.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_trading_dates.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_trading_monitor.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_trading_monitor_browser.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_tutorial_reader.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_tx_auto_import.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_tx_shutdown.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/trading_calendar.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/trading_monitor.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tx.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/xtconstant.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/xtdata.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/xttype.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant.egg-info/dependency_links.txt +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant.egg-info/entry_points.txt +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant.egg-info/top_level.txt +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/cfquant_pipe_hub.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/qmt_scripts/CFQUANT.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/qmt_scripts/__init__.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_CTYPE_ALL_LOWLAT_SH.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_CTYPE_ALL_LOWLAT_SZ.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_TRADE_LOWLAT_SH.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_TRADE_LOWLAT_SZ.py" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/readme.md" +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/setup.cfg +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/__init__.py +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/api-tester.js +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/avatars/amber-pulse.svg +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/avatars/copper-grid.svg +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/avatars/market-blue.svg +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/avatars/rose-circuit.svg +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/avatars/signal-green.svg +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/avatars/slate-wave.svg +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/avatars/teal-orbit.svg +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/avatars/violet-node.svg +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/cftrader-api-data.js +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/docs.js +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/favicon.svg +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/python-api-data.js +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/python-api.js +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/remote-management.js +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/styles.css +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/trading-monitor.js +0 -0
- {cfquant-0.2.59 → cfquant-0.2.61}/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.61
|
|
4
4
|
Summary: QMT local bridge and xtquant-compatible Python client.
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.8
|
|
@@ -15,7 +15,7 @@ Requires-Dist: pytz
|
|
|
15
15
|
Requires-Dist: tabulate
|
|
16
16
|
Requires-Dist: pandas-market-calendars
|
|
17
17
|
Requires-Dist: importlib-resources>=5
|
|
18
|
-
Requires-Dist: frida
|
|
18
|
+
Requires-Dist: frida==17.16.4; sys_platform == "win32"
|
|
19
19
|
Requires-Dist: pywin32>=306; sys_platform == "win32"
|
|
20
20
|
Provides-Extra: dev
|
|
21
21
|
Requires-Dist: build>=1.0; extra == "dev"
|
|
@@ -10,9 +10,9 @@ import tempfile
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
|
|
12
12
|
if __package__:
|
|
13
|
-
from .qmt_dependencies import report_startup_dependencies
|
|
13
|
+
from .qmt_dependencies import FRIDA_REQUIREMENT, FRIDA_VERSION, report_startup_dependencies
|
|
14
14
|
else:
|
|
15
|
-
from qmt_dependencies import report_startup_dependencies
|
|
15
|
+
from qmt_dependencies import FRIDA_REQUIREMENT, FRIDA_VERSION, report_startup_dependencies
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
PACKAGE_NAME = "cfquant"
|
|
@@ -517,6 +517,32 @@ def installed_package_version(python_exe=None):
|
|
|
517
517
|
return completed.stdout.strip() if completed.returncode == 0 else ""
|
|
518
518
|
|
|
519
519
|
|
|
520
|
+
def reconcile_startup_frida(project_root, python_exe):
|
|
521
|
+
"""Repair version drift even when cfquant's own version did not change.
|
|
522
|
+
|
|
523
|
+
This launcher runs before Web/Frida loads. Failed optional repair must not
|
|
524
|
+
prevent manual QMT use; the native worker also checks before importing.
|
|
525
|
+
"""
|
|
526
|
+
report = report_startup_dependencies(python_exe) or {}
|
|
527
|
+
if os.environ.get("CFQUANT_QMT_AUTOMATION", "1").lower() in ("0", "false", "off"):
|
|
528
|
+
return
|
|
529
|
+
if not report.get("supported") or not report.get("packages"):
|
|
530
|
+
return
|
|
531
|
+
if report["packages"].get("frida") == FRIDA_VERSION:
|
|
532
|
+
return
|
|
533
|
+
print("[INFO] Installing QMT compatibility dependency: %s" % FRIDA_REQUIREMENT)
|
|
534
|
+
command = [python_exe, "-m", "pip", "install", "--disable-pip-version-check", "--no-input",
|
|
535
|
+
"--timeout", "15", "--retries", "1"] + pip_index_args() + [FRIDA_REQUIREMENT]
|
|
536
|
+
try:
|
|
537
|
+
completed = subprocess.run(command, cwd=str(project_root), env=pip_environment(),
|
|
538
|
+
timeout=120, **_hidden_subprocess_kwargs())
|
|
539
|
+
if completed.returncode:
|
|
540
|
+
raise RuntimeError("pip exit code %s" % completed.returncode)
|
|
541
|
+
except (OSError, RuntimeError, subprocess.TimeoutExpired) as error:
|
|
542
|
+
print("[WARN] Frida compatibility repair failed: %s. Web can continue; use manual QMT login." % error)
|
|
543
|
+
report_startup_dependencies(python_exe)
|
|
544
|
+
|
|
545
|
+
|
|
520
546
|
def ensure_cfquant_installed(project_root, python_exe=None):
|
|
521
547
|
project_root = Path(project_root).resolve()
|
|
522
548
|
if not (project_root / "pyproject.toml").is_file():
|
|
@@ -546,7 +572,7 @@ def ensure_cfquant_installed(project_root, python_exe=None):
|
|
|
546
572
|
print("cfquant installed=%s project=%s" % (installed or "missing", expected))
|
|
547
573
|
if installed == expected:
|
|
548
574
|
print("Package versions match; skip installation.")
|
|
549
|
-
|
|
575
|
+
reconcile_startup_frida(project_root, python_exe)
|
|
550
576
|
return 0
|
|
551
577
|
|
|
552
578
|
command = editable_install_args(project_root, python_exe)
|
|
@@ -570,7 +596,7 @@ def ensure_cfquant_installed(project_root, python_exe=None):
|
|
|
570
596
|
print("[ERROR] Installed cfquant version does not match the project after installation.", file=sys.stderr)
|
|
571
597
|
return 1
|
|
572
598
|
print("%s package is installed." % PACKAGE_NAME)
|
|
573
|
-
|
|
599
|
+
reconcile_startup_frida(project_root, python_exe)
|
|
574
600
|
return 0
|
|
575
601
|
|
|
576
602
|
|
|
@@ -194,7 +194,7 @@ class NormalQmtBridge(TxTradeBridge):
|
|
|
194
194
|
if action == "cfquant.status":
|
|
195
195
|
self._send_response(msg, self._status())
|
|
196
196
|
return
|
|
197
|
-
if action
|
|
197
|
+
if action in ("cfquant.strategy.control", "cfquant.account_subscription_status"):
|
|
198
198
|
try:
|
|
199
199
|
self._send_response(msg, self._dispatch(action, msg.get("params") or {}, msg))
|
|
200
200
|
except Exception as error:
|
|
@@ -48,8 +48,16 @@ class PipeNormalQmtBridge(NormalQmtBridge):
|
|
|
48
48
|
self.pipe_name = pipe_name or DEFAULT_PIPE_NAME
|
|
49
49
|
self.request_channels = list(request_channels or [request_channel])
|
|
50
50
|
self.connect_timeout_ms = int(connect_timeout_ms)
|
|
51
|
+
self._pipe_lock = threading.RLock()
|
|
52
|
+
self._pipe_closed = False
|
|
51
53
|
|
|
52
54
|
def start(self):
|
|
55
|
+
with self._pipe_lock:
|
|
56
|
+
if self._pipe_closed:
|
|
57
|
+
raise RuntimeError("pipe bridge is closed")
|
|
58
|
+
return self._start_transport()
|
|
59
|
+
|
|
60
|
+
def _start_transport(self):
|
|
53
61
|
runtime = self.globals_dict.get("_managed_runtime")
|
|
54
62
|
if runtime is not None:
|
|
55
63
|
runtime.attach_bridge(self)
|
|
@@ -80,23 +88,31 @@ class PipeNormalQmtBridge(NormalQmtBridge):
|
|
|
80
88
|
self._publish_runtime_report("start")
|
|
81
89
|
return self
|
|
82
90
|
|
|
91
|
+
def stop_transport(self):
|
|
92
|
+
# This phase must not wait for QMT native work or dispatch_lock.
|
|
93
|
+
with self._pipe_lock:
|
|
94
|
+
self._pipe_closed = True
|
|
95
|
+
self.running = False
|
|
96
|
+
tx, self.tx = self.tx, None
|
|
97
|
+
event = getattr(self, "worker_event", None)
|
|
98
|
+
if event is not None:
|
|
99
|
+
event.set()
|
|
100
|
+
if tx is not None:
|
|
101
|
+
tx.close()
|
|
102
|
+
deadline = time.monotonic() + 0.5
|
|
103
|
+
for worker in (getattr(self, "recv_thread", None), getattr(self, "worker_thread", None)):
|
|
104
|
+
if worker is not None and worker is not threading.current_thread() and worker.is_alive():
|
|
105
|
+
worker.join(max(0.0, deadline - time.monotonic()))
|
|
106
|
+
|
|
83
107
|
def close(self):
|
|
108
|
+
self.stop_transport()
|
|
84
109
|
self._flush_pending_order_errors(force=True)
|
|
85
|
-
self.running = False
|
|
86
|
-
self.worker_event.set()
|
|
87
110
|
self._close_quote_subscriptions()
|
|
88
111
|
if self.context is not None and self.schedule_key:
|
|
89
112
|
try:
|
|
90
113
|
self.context.cancel_schedule_run(self.schedule_key)
|
|
91
114
|
except Exception:
|
|
92
115
|
pass
|
|
93
|
-
tx = self.tx
|
|
94
|
-
self.tx = None
|
|
95
|
-
if tx is not None:
|
|
96
|
-
try:
|
|
97
|
-
tx.close()
|
|
98
|
-
except Exception:
|
|
99
|
-
pass
|
|
100
116
|
self._log("pipe normal bridge stopped")
|
|
101
117
|
|
|
102
118
|
def _status_extra(self):
|
|
@@ -138,8 +154,16 @@ class PipeTradeBridge(TxTradeBridge):
|
|
|
138
154
|
)
|
|
139
155
|
self.pipe_name = pipe_name or DEFAULT_PIPE_NAME
|
|
140
156
|
self.connect_timeout_ms = int(connect_timeout_ms)
|
|
157
|
+
self._pipe_lock = threading.RLock()
|
|
158
|
+
self._pipe_closed = False
|
|
141
159
|
|
|
142
160
|
def start(self):
|
|
161
|
+
with self._pipe_lock:
|
|
162
|
+
if self._pipe_closed:
|
|
163
|
+
raise RuntimeError("pipe bridge is closed")
|
|
164
|
+
return self._start_transport()
|
|
165
|
+
|
|
166
|
+
def _start_transport(self):
|
|
143
167
|
runtime = self.globals_dict.get("_managed_runtime")
|
|
144
168
|
if runtime is not None:
|
|
145
169
|
runtime.attach_bridge(self)
|
|
@@ -166,15 +190,11 @@ class PipeTradeBridge(TxTradeBridge):
|
|
|
166
190
|
self._publish_runtime_report("start")
|
|
167
191
|
return self
|
|
168
192
|
|
|
193
|
+
def stop_transport(self):
|
|
194
|
+
PipeNormalQmtBridge.stop_transport(self)
|
|
195
|
+
|
|
169
196
|
def close(self):
|
|
170
|
-
self.
|
|
171
|
-
tx = self.tx
|
|
172
|
-
self.tx = None
|
|
173
|
-
if tx is not None:
|
|
174
|
-
try:
|
|
175
|
-
tx.close()
|
|
176
|
-
except Exception:
|
|
177
|
-
pass
|
|
197
|
+
self.stop_transport()
|
|
178
198
|
self._log("pipe trade bridge stopped")
|
|
179
199
|
|
|
180
200
|
def poll(self, max_messages=100, timeout=0):
|
|
@@ -254,12 +254,14 @@ class NamedPipeConnection(object):
|
|
|
254
254
|
offset += written.value
|
|
255
255
|
|
|
256
256
|
|
|
257
|
-
def connect_pipe(pipe_name=None, timeout_ms=3000):
|
|
257
|
+
def connect_pipe(pipe_name=None, timeout_ms=3000, stop_event=None):
|
|
258
258
|
pipe_name = normalize_pipe_name(pipe_name)
|
|
259
259
|
k32 = kernel32()
|
|
260
|
-
deadline = time.
|
|
260
|
+
deadline = time.monotonic() + max(float(timeout_ms), 1.0) / 1000.0
|
|
261
261
|
last_error = None
|
|
262
262
|
while True:
|
|
263
|
+
if stop_event is not None and stop_event.is_set():
|
|
264
|
+
raise ConnectionAbortedError("named pipe connection cancelled")
|
|
263
265
|
handle = k32.dll.CreateFileW(
|
|
264
266
|
pipe_name,
|
|
265
267
|
GENERIC_READ | GENERIC_WRITE,
|
|
@@ -270,16 +272,23 @@ def connect_pipe(pipe_name=None, timeout_ms=3000):
|
|
|
270
272
|
None,
|
|
271
273
|
)
|
|
272
274
|
if not k32.invalid_handle(handle):
|
|
273
|
-
|
|
275
|
+
conn = NamedPipeConnection(handle, pipe_name, owner_server_side=False)
|
|
276
|
+
if stop_event is not None and stop_event.is_set():
|
|
277
|
+
conn.close()
|
|
278
|
+
raise ConnectionAbortedError("named pipe connection cancelled")
|
|
279
|
+
return conn
|
|
274
280
|
last_error = k32.last_error()
|
|
275
281
|
if last_error == ERROR_PIPE_BUSY:
|
|
276
|
-
wait_ms = min(
|
|
282
|
+
wait_ms = min(50 if stop_event is not None else 250,
|
|
283
|
+
max(1, int((deadline - time.monotonic()) * 1000)))
|
|
277
284
|
k32.dll.WaitNamedPipeW(pipe_name, wait_ms)
|
|
278
|
-
elif
|
|
279
|
-
|
|
285
|
+
elif stop_event is not None:
|
|
286
|
+
stop_event.wait(0.05)
|
|
280
287
|
else:
|
|
281
288
|
time.sleep(0.05)
|
|
282
|
-
if
|
|
289
|
+
if stop_event is not None and stop_event.is_set():
|
|
290
|
+
raise ConnectionAbortedError("named pipe connection cancelled")
|
|
291
|
+
if time.monotonic() >= deadline:
|
|
283
292
|
raise OSError(last_error or 0, "connect named pipe timeout pipe=%s winerror=%s" % (pipe_name, last_error))
|
|
284
293
|
|
|
285
294
|
|
|
@@ -366,21 +375,27 @@ class PipeTxClient(object):
|
|
|
366
375
|
self._sender = None
|
|
367
376
|
self._sender_conn = None
|
|
368
377
|
self._stop_event = threading.Event()
|
|
378
|
+
self._closed = False
|
|
379
|
+
self._connecting_conns = set()
|
|
369
380
|
self.send_max_bytes = int(os.environ.get("CFQUANT_PIPE_SEND_MAX_BYTES", 128 * 1024 * 1024))
|
|
370
381
|
self.send_timeout = float(os.environ.get("CFQUANT_PIPE_SEND_TIMEOUT", "15"))
|
|
371
382
|
|
|
372
383
|
def start(self):
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
self.
|
|
382
|
-
self.
|
|
383
|
-
self.
|
|
384
|
+
with self.conn_lock:
|
|
385
|
+
# A strategy restart creates a new client. Reusing this stop event
|
|
386
|
+
# could reactivate workers still exiting from the previous run.
|
|
387
|
+
if self._closed:
|
|
388
|
+
raise RuntimeError("pipe tx client is closed")
|
|
389
|
+
if self.running:
|
|
390
|
+
return self
|
|
391
|
+
self.running = True
|
|
392
|
+
self.thread = threading.Thread(target=self._connect_loop, name="cfquant-pipe-connect")
|
|
393
|
+
self.thread.daemon = True
|
|
394
|
+
self.thread.start()
|
|
395
|
+
if self.heartbeat_interval > 0 or self.send_timeout > 0:
|
|
396
|
+
self.heartbeat_thread = threading.Thread(target=self._heartbeat_loop, name="cfquant-pipe-heartbeat")
|
|
397
|
+
self.heartbeat_thread.daemon = True
|
|
398
|
+
self.heartbeat_thread.start()
|
|
384
399
|
return self
|
|
385
400
|
|
|
386
401
|
def start_tx(self):
|
|
@@ -406,6 +421,8 @@ class PipeTxClient(object):
|
|
|
406
421
|
if not isinstance(payload, str):
|
|
407
422
|
raise TypeError("pipe publish payload must be an encoded string")
|
|
408
423
|
with self.conn_lock:
|
|
424
|
+
if self._closed:
|
|
425
|
+
return {"code": -1, "msg": "pipe tx client is closed"}
|
|
409
426
|
if (self.tx_conn, self.connection_generation) != (conn, generation):
|
|
410
427
|
raise RuntimeError("pipe connection changed before send")
|
|
411
428
|
if self._sender is None or self._sender_conn is not conn:
|
|
@@ -430,25 +447,59 @@ class PipeTxClient(object):
|
|
|
430
447
|
return {"code": -1, "msg": str(e)}
|
|
431
448
|
|
|
432
449
|
def close(self):
|
|
433
|
-
self.
|
|
434
|
-
|
|
450
|
+
with self.conn_lock:
|
|
451
|
+
self._closed = True
|
|
452
|
+
self.running = False
|
|
453
|
+
self._stop_event.set()
|
|
454
|
+
connecting = tuple(self._connecting_conns)
|
|
455
|
+
self._connecting_conns.clear()
|
|
456
|
+
sender = self._sender
|
|
457
|
+
workers = (self.thread, self.heartbeat_thread)
|
|
458
|
+
# Interrupt ReadFile/WriteFile, including either hello handshake,
|
|
459
|
+
# before joining. Native cleanup and joins must not hold conn_lock.
|
|
460
|
+
self._drop_pair(None, reason="pipe tx client closed", conns=connecting)
|
|
461
|
+
# A concurrent submit may have started the sender just before it was
|
|
462
|
+
# closed. Read its thread only after close prevents further submits.
|
|
463
|
+
workers += (getattr(sender, "_thread", None),)
|
|
435
464
|
try:
|
|
436
465
|
self.Q.put_nowait(None)
|
|
437
|
-
except
|
|
466
|
+
except queue.Full:
|
|
438
467
|
pass
|
|
439
|
-
|
|
468
|
+
deadline = time.monotonic() + 1.0
|
|
469
|
+
for worker in workers:
|
|
470
|
+
if worker is not None and worker is not threading.current_thread() and worker.is_alive():
|
|
471
|
+
worker.join(max(0.0, deadline - time.monotonic()))
|
|
472
|
+
|
|
473
|
+
def _open_connection(self, role):
|
|
474
|
+
conn = connect_pipe(self.pipe_name, timeout_ms=self.connect_timeout_ms,
|
|
475
|
+
stop_event=self._stop_event)
|
|
476
|
+
try:
|
|
477
|
+
with self.conn_lock:
|
|
478
|
+
if self._closed:
|
|
479
|
+
raise ConnectionAbortedError("pipe tx client is closed")
|
|
480
|
+
self._connecting_conns.add(conn)
|
|
481
|
+
conn.write_frame(dumps_pipe_message(self._pipe_envelope("hello", role)))
|
|
482
|
+
if self._stop_event.is_set():
|
|
483
|
+
raise ConnectionAbortedError("pipe tx client is closed")
|
|
484
|
+
return conn
|
|
485
|
+
except BaseException:
|
|
486
|
+
with self.conn_lock:
|
|
487
|
+
self._connecting_conns.discard(conn)
|
|
488
|
+
self._close_conns((conn,))
|
|
489
|
+
raise
|
|
440
490
|
|
|
441
491
|
def _connect_loop(self):
|
|
442
|
-
while self.running:
|
|
492
|
+
while self.running and not self._stop_event.is_set():
|
|
443
493
|
rx_conn = None
|
|
444
494
|
tx_conn = None
|
|
445
495
|
generation = None
|
|
446
496
|
try:
|
|
447
|
-
rx_conn =
|
|
448
|
-
|
|
449
|
-
tx_conn = connect_pipe(self.pipe_name, timeout_ms=self.connect_timeout_ms)
|
|
450
|
-
tx_conn.write_frame(dumps_pipe_message(self._pipe_envelope("hello", "qmt_tx")))
|
|
497
|
+
rx_conn = self._open_connection("qmt_rx")
|
|
498
|
+
tx_conn = self._open_connection("qmt_tx")
|
|
451
499
|
with self.conn_lock:
|
|
500
|
+
if self._closed:
|
|
501
|
+
raise ConnectionAbortedError("pipe tx client is closed")
|
|
502
|
+
self._connecting_conns.difference_update((rx_conn, tx_conn))
|
|
452
503
|
self.connection_generation += 1
|
|
453
504
|
generation = self.connection_generation
|
|
454
505
|
self.rx_conn = rx_conn
|
|
@@ -461,29 +512,44 @@ class PipeTxClient(object):
|
|
|
461
512
|
)
|
|
462
513
|
self._read_loop(rx_conn, generation)
|
|
463
514
|
except Exception as e:
|
|
464
|
-
self.
|
|
465
|
-
|
|
466
|
-
|
|
515
|
+
with self.conn_lock:
|
|
516
|
+
if self.running and not self._closed:
|
|
517
|
+
self.last_error = str(e)
|
|
518
|
+
self._log("pipe connect/read failed: %s" % e)
|
|
467
519
|
finally:
|
|
520
|
+
with self.conn_lock:
|
|
521
|
+
self._connecting_conns.difference_update((rx_conn, tx_conn))
|
|
468
522
|
if generation is None:
|
|
469
523
|
self._close_conns((rx_conn, tx_conn))
|
|
470
524
|
else:
|
|
471
525
|
self._drop_pair(generation, conns=(rx_conn, tx_conn))
|
|
472
|
-
if self.
|
|
473
|
-
|
|
526
|
+
if self._stop_event.wait(max(0.0, self.reconnect_interval)):
|
|
527
|
+
break
|
|
474
528
|
|
|
475
529
|
def _read_loop(self, conn, generation):
|
|
476
|
-
while self.running and self._get_rx_state() == (conn, generation):
|
|
530
|
+
while self.running and not self._stop_event.is_set() and self._get_rx_state() == (conn, generation):
|
|
477
531
|
raw = conn.read_frame()
|
|
478
532
|
if raw is None:
|
|
479
533
|
break
|
|
480
534
|
envelope = loads_pipe_message(raw)
|
|
481
535
|
if envelope:
|
|
482
536
|
payload = envelope.get("payload")
|
|
483
|
-
if payload:
|
|
484
|
-
|
|
537
|
+
if payload and not self._queue_received(payload):
|
|
538
|
+
return
|
|
485
539
|
continue
|
|
486
|
-
self.
|
|
540
|
+
if not self._queue_received(raw):
|
|
541
|
+
return
|
|
542
|
+
|
|
543
|
+
def _queue_received(self, payload):
|
|
544
|
+
# A stopped bridge no longer drains Q. Never leave the reader blocked
|
|
545
|
+
# forever in put() when its bounded queue is full at shutdown.
|
|
546
|
+
while self.running and not self._stop_event.is_set():
|
|
547
|
+
try:
|
|
548
|
+
self.Q.put(payload, timeout=0.1)
|
|
549
|
+
return True
|
|
550
|
+
except queue.Full:
|
|
551
|
+
pass
|
|
552
|
+
return False
|
|
487
553
|
|
|
488
554
|
def _get_rx_conn(self):
|
|
489
555
|
with self.conn_lock:
|
|
@@ -154,7 +154,8 @@ def is_readonly_request(action):
|
|
|
154
154
|
# Only known query families. Downloads, subscriptions and all writes retain
|
|
155
155
|
# their existing execution semantics, including on an ambiguous timeout.
|
|
156
156
|
return (str(action).startswith(("xtdata.get_", "xttrader.query_"))
|
|
157
|
-
or action in ("cfquant.ping", "cfquant.status", "cfquant.query_info"
|
|
157
|
+
or action in ("cfquant.ping", "cfquant.status", "cfquant.query_info",
|
|
158
|
+
"cfquant.account_subscription_status"))
|
|
158
159
|
|
|
159
160
|
|
|
160
161
|
def request_expired(msg, now=None):
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""One-time migration of QMT automation preferences, using saved version evidence."""
|
|
2
|
+
import copy
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
AUTOMATION_DEFAULTS_VERSION = "0.2.59"
|
|
7
|
+
MIGRATION_KEY = "qmt_automation_defaults_migration"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def is_pre_automation_defaults_version(value):
|
|
11
|
+
"""Schema numbers, missing values and unrecognized versions are not evidence."""
|
|
12
|
+
match = re.fullmatch(r"v?(\d+)\.(\d+)\.(\d+)(?:(a|b|rc)\d+)?(?:\.post\d+)?(?:\+[-\w.]+)?", str(value or "").strip())
|
|
13
|
+
if not match:
|
|
14
|
+
return False
|
|
15
|
+
version = tuple(int(match.group(i)) for i in (1, 2, 3))
|
|
16
|
+
return version < (0, 2, 59) or (version == (0, 2, 59) and bool(match.group(4)))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def migrate_qmt_automation_defaults(raw):
|
|
20
|
+
"""Return a copy only when a known old configuration needs migration.
|
|
21
|
+
|
|
22
|
+
The running/installed version and QMT's version do not date user preferences.
|
|
23
|
+
They must never be used as substitutes for the saved configuration version.
|
|
24
|
+
"""
|
|
25
|
+
if not isinstance(raw, dict) or raw.get(MIGRATION_KEY):
|
|
26
|
+
return raw, 0
|
|
27
|
+
source_version = raw.get("saved_core_version", raw.get("core_version"))
|
|
28
|
+
if not is_pre_automation_defaults_version(source_version):
|
|
29
|
+
return raw, 0
|
|
30
|
+
result = copy.deepcopy(raw)
|
|
31
|
+
changed = 0
|
|
32
|
+
for collection in ("account_configs", "account_pairs"):
|
|
33
|
+
rows = result.get(collection) or {}
|
|
34
|
+
rows = rows.values() if isinstance(rows, dict) else rows if isinstance(rows, list) else []
|
|
35
|
+
for row in rows:
|
|
36
|
+
if not isinstance(row, dict) or not row.get("account_id"):
|
|
37
|
+
continue
|
|
38
|
+
login = row.get("qmt_auto_login")
|
|
39
|
+
login = dict(login) if isinstance(login, dict) else {}
|
|
40
|
+
login.update(enabled=True, auto_update=True)
|
|
41
|
+
# Preserve custom schedules and explicit empty schedules. Only absent
|
|
42
|
+
# schedules receive the same default as a new binding.
|
|
43
|
+
if not any(key in login for key in ("restart_times", "restartTimes", "restart_time", "restartTime")):
|
|
44
|
+
login["restart_times"] = ["09:00"]
|
|
45
|
+
row["qmt_auto_login"] = login
|
|
46
|
+
if collection == "account_configs":
|
|
47
|
+
strategy = row.get("qmt_strategy")
|
|
48
|
+
strategy = dict(strategy) if isinstance(strategy, dict) else {}
|
|
49
|
+
strategy.update(enabled=True, autorun=True)
|
|
50
|
+
row["qmt_strategy"] = strategy
|
|
51
|
+
changed += 1
|
|
52
|
+
result[MIGRATION_KEY] = {"version": AUTOMATION_DEFAULTS_VERSION, "source_version": str(source_version),
|
|
53
|
+
"bindings": changed}
|
|
54
|
+
return result, changed
|
|
@@ -11,6 +11,8 @@ from pathlib import Path
|
|
|
11
11
|
import sys
|
|
12
12
|
import time
|
|
13
13
|
|
|
14
|
+
from .qmt_native_worker import IsolatedLoginBackend, QmtAutomationCancelled, verify_environment_isolated
|
|
15
|
+
|
|
14
16
|
|
|
15
17
|
class QmtNotReady(RuntimeError):
|
|
16
18
|
pass
|
|
@@ -22,6 +24,11 @@ class QmtProcessExited(RuntimeError):
|
|
|
22
24
|
|
|
23
25
|
def verify_environment(directory):
|
|
24
26
|
"""Validate dependencies and native ABI before stopping a running terminal."""
|
|
27
|
+
verify_environment_isolated(directory)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _verify_environment_native(directory):
|
|
31
|
+
"""Only invoked inside the native worker or the standalone login CLI."""
|
|
25
32
|
try:
|
|
26
33
|
from .qmt_dependencies import LOGIN_MODULES, import_login_module
|
|
27
34
|
except ImportError:
|
|
@@ -163,7 +170,7 @@ class QtLoginBackend:
|
|
|
163
170
|
self.pid = matches[0].pid
|
|
164
171
|
if expected_pid is not None and self.pid != expected_pid:
|
|
165
172
|
raise RuntimeError("QMT process changed; stopping this login attempt")
|
|
166
|
-
|
|
173
|
+
_verify_environment_native(directory)
|
|
167
174
|
self.mutex = win32event.CreateMutex(None, False, "Local\\CFQuantQmtComponentLogin_" + str(self.pid))
|
|
168
175
|
acquired = win32event.WaitForSingleObject(self.mutex, 0)
|
|
169
176
|
if acquired not in (0, 0x80):
|
|
@@ -367,8 +374,11 @@ def login_running_qmt(directory, login_account="", password_provider=lambda: "",
|
|
|
367
374
|
if should_cancel():
|
|
368
375
|
return {"outcome": "cancelled", "login_succeeded": False, "attempts": []}
|
|
369
376
|
try:
|
|
370
|
-
backend =
|
|
377
|
+
backend = IsolatedLoginBackend(Path(directory), timeout, expected_pid=expected_pid,
|
|
378
|
+
should_cancel=should_cancel)
|
|
371
379
|
break
|
|
380
|
+
except QmtAutomationCancelled:
|
|
381
|
+
return {"outcome": "cancelled", "login_succeeded": False, "attempts": []}
|
|
372
382
|
except QmtProcessExited:
|
|
373
383
|
return {"outcome": "process_exited", "login_succeeded": False, "attempts": []}
|
|
374
384
|
except QmtNotReady:
|
|
@@ -380,6 +390,8 @@ def login_running_qmt(directory, login_account="", password_provider=lambda: "",
|
|
|
380
390
|
backend.should_cancel = should_cancel
|
|
381
391
|
return auto_login(backend, password_provider, retry_not_ready=True, login_account=login_account,
|
|
382
392
|
should_cancel=should_cancel, on_wait=on_wait)
|
|
393
|
+
except QmtAutomationCancelled:
|
|
394
|
+
return {"outcome": "cancelled", "login_succeeded": False, "attempts": []}
|
|
383
395
|
except QmtProcessExited:
|
|
384
396
|
return {"outcome": "process_exited", "login_succeeded": False, "attempts": []}
|
|
385
397
|
finally:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Read-only checks and repair instructions for optional QMT login components."""
|
|
2
2
|
import copy
|
|
3
3
|
import importlib
|
|
4
|
+
import importlib.metadata
|
|
4
5
|
import json
|
|
5
6
|
import os
|
|
6
7
|
import subprocess
|
|
@@ -10,6 +11,10 @@ import time
|
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
LOGIN_MODULES = ("frida", "win32api", "win32crypt", "win32event", "win32gui", "win32process")
|
|
14
|
+
# Verified QMT baseline. 17.25 introduced a Windows mapper that can abort the
|
|
15
|
+
# host on older ntdll builds; do not widen this without Windows/QMT validation.
|
|
16
|
+
FRIDA_VERSION = "17.16.4"
|
|
17
|
+
FRIDA_REQUIREMENT = "frida==" + FRIDA_VERSION
|
|
13
18
|
_LOCK = threading.Lock()
|
|
14
19
|
_CACHE = {}
|
|
15
20
|
_PROBE_CODE = r'''
|
|
@@ -17,6 +22,8 @@ import importlib, importlib.metadata, json, os, pathlib, sys
|
|
|
17
22
|
checks = []
|
|
18
23
|
for name in MODULES:
|
|
19
24
|
try:
|
|
25
|
+
if name == 'frida' and importlib.metadata.version('frida') != FRIDA_VERSION:
|
|
26
|
+
raise RuntimeError('Unsupported Frida version; install frida==' + FRIDA_VERSION)
|
|
20
27
|
module = importlib.import_module(name)
|
|
21
28
|
checks.append(dict(name=name, ok=True, path=getattr(module, '__file__', '')))
|
|
22
29
|
except Exception as error:
|
|
@@ -46,6 +53,16 @@ def _ps_quote(value):
|
|
|
46
53
|
|
|
47
54
|
|
|
48
55
|
def _finish_report(report):
|
|
56
|
+
if report.get("supported"):
|
|
57
|
+
version = (report.get("packages") or {}).get("frida")
|
|
58
|
+
if version and version != FRIDA_VERSION:
|
|
59
|
+
checks = report.setdefault("checks", [])
|
|
60
|
+
check = next((item for item in checks if item["name"] == "frida"), None)
|
|
61
|
+
if check is None:
|
|
62
|
+
check = {"name": "frida"}
|
|
63
|
+
checks.append(check)
|
|
64
|
+
check.update(ok=False, error="Frida %s 未通过本项目兼容性验证,请安装 %s" % (version, FRIDA_REQUIREMENT))
|
|
65
|
+
report["frida_requirement"] = FRIDA_REQUIREMENT
|
|
49
66
|
report["checked_at"] = time.time()
|
|
50
67
|
report["ok"] = bool(report.get("supported") and not report.get("error")
|
|
51
68
|
and all(item["ok"] for item in report.get("checks", [])))
|
|
@@ -73,8 +90,10 @@ def _finish_report(report):
|
|
|
73
90
|
if environment == "python":
|
|
74
91
|
commands.append(python + " -m pywin32_postinstall -install")
|
|
75
92
|
if any(item["name"] == "frida" for item in failed):
|
|
76
|
-
commands.append(python + " -m pip install --no-cache-dir --
|
|
77
|
-
commands.append(python + " -c " + _ps_quote(
|
|
93
|
+
commands.append(python + " -m pip install --no-cache-dir --force-reinstall '" + FRIDA_REQUIREMENT + "'")
|
|
94
|
+
commands.append(python + " -c " + _ps_quote(
|
|
95
|
+
"from cfquant.qmt_dependencies import report_startup_dependencies; "
|
|
96
|
+
"r=report_startup_dependencies(); raise SystemExit(0 if r['ok'] else 1)"))
|
|
78
97
|
report["repair_commands"] = commands
|
|
79
98
|
if environment == "conda":
|
|
80
99
|
report["repair_note"] = (
|
|
@@ -111,7 +130,7 @@ def check_login_dependencies(python_exe=None, refresh=False, timeout=15):
|
|
|
111
130
|
if os.name == "nt":
|
|
112
131
|
run_kwargs["creationflags"] = getattr(subprocess, "CREATE_NO_WINDOW", 0)
|
|
113
132
|
completed = subprocess.run(
|
|
114
|
-
[executable, "-c", "MODULES = %r\n" % (LOGIN_MODULES,) + _PROBE_CODE],
|
|
133
|
+
[executable, "-c", "MODULES = %r\nFRIDA_VERSION = %r\n" % (LOGIN_MODULES, FRIDA_VERSION) + _PROBE_CODE],
|
|
115
134
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding="utf-8",
|
|
116
135
|
errors="replace", timeout=timeout,
|
|
117
136
|
**run_kwargs,
|
|
@@ -130,8 +149,21 @@ class QmtDependencyError(RuntimeError):
|
|
|
130
149
|
pass
|
|
131
150
|
|
|
132
151
|
|
|
152
|
+
def require_compatible_frida():
|
|
153
|
+
try:
|
|
154
|
+
version = importlib.metadata.version("frida")
|
|
155
|
+
except importlib.metadata.PackageNotFoundError:
|
|
156
|
+
version = "未安装"
|
|
157
|
+
if version != FRIDA_VERSION:
|
|
158
|
+
raise QmtDependencyError(
|
|
159
|
+
"Frida %s 未通过本项目兼容性验证,已跳过 QMT 自动操作。"
|
|
160
|
+
"请在当前 Python 环境安装 %s;手动登录及行情交易接口仍可使用。" % (version, FRIDA_REQUIREMENT))
|
|
161
|
+
|
|
162
|
+
|
|
133
163
|
def import_login_module(name):
|
|
134
164
|
"""Keep in-process import errors actionable, including stale loaded DLLs."""
|
|
165
|
+
if name == "frida":
|
|
166
|
+
require_compatible_frida()
|
|
135
167
|
try:
|
|
136
168
|
return importlib.import_module(name)
|
|
137
169
|
except (ImportError, OSError) as error:
|