cfquant 0.2.55__tar.gz → 0.2.57__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.
Files changed (219) hide show
  1. {cfquant-0.2.55 → cfquant-0.2.57}/PKG-INFO +4 -1
  2. {cfquant-0.2.55 → cfquant-0.2.57}/README.md +5 -3
  3. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/_editable_install.py +40 -2
  4. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/batch_orders.py +15 -0
  5. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/cftrader.py +21 -3
  6. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/client.py +13 -3
  7. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/management.py +16 -0
  8. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/normal_bridge.py +24 -0
  9. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/pipe_client.py +20 -13
  10. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/protocol.py +5 -4
  11. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/qmt_component_agent.js +11 -2
  12. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/qmt_component_login.py +109 -41
  13. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/qmt_dependencies.py +33 -12
  14. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/qmt_pipe_runtime.py +28 -5
  15. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/qmt_strategy_deploy.py +66 -35
  16. cfquant-0.2.57/cfquant/qmt_update.py +366 -0
  17. cfquant-0.2.57/cfquant/remote_adapter.py +265 -0
  18. cfquant-0.2.57/cfquant/remote_management.py +407 -0
  19. cfquant-0.2.57/cfquant/remote_supervisor.py +120 -0
  20. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/rpc_timing.py +15 -0
  21. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/16_/345/220/257/345/212/250/345/256/211/350/243/205/346/265/213/350/257/225.py +36 -1
  22. {cfquant-0.2.55 → cfquant-0.2.57}/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
  23. cfquant-0.2.57/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
  24. cfquant-0.2.57/cfquant/tests/test_account_query_timing.py +131 -0
  25. cfquant-0.2.57/cfquant/tests/test_binding_live_refresh_browser.py +95 -0
  26. cfquant-0.2.57/cfquant/tests/test_callback_recovery.py +287 -0
  27. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_cftrader.py +17 -1
  28. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_cftrader_tutorial.py +5 -0
  29. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_market_timing.py +1 -1
  30. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_qmt_auto_login_browser.py +87 -0
  31. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_qmt_auto_login_integration.py +121 -3
  32. cfquant-0.2.57/cfquant/tests/test_qmt_component_login.py +347 -0
  33. cfquant-0.2.57/cfquant/tests/test_qmt_dependencies.py +107 -0
  34. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_qmt_fileless.py +129 -3
  35. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_qmt_strategy_browser.py +19 -10
  36. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_qmt_strategy_deploy.py +29 -0
  37. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_qmt_strategy_web.py +3 -3
  38. cfquant-0.2.57/cfquant/tests/test_qmt_update.py +74 -0
  39. cfquant-0.2.57/cfquant/tests/test_remote_management.py +352 -0
  40. cfquant-0.2.57/cfquant/tests/test_remote_management_browser.py +40 -0
  41. cfquant-0.2.57/cfquant/tests/test_remote_version_check.py +72 -0
  42. cfquant-0.2.57/cfquant/tests/test_trader_subscription_recovery.py +345 -0
  43. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_tutorial_reader.py +3 -0
  44. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tx_trade_bridge.py +36 -12
  45. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/version.py +2 -2
  46. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/xttrader.py +201 -43
  47. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant.egg-info/PKG-INFO +4 -1
  48. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant.egg-info/SOURCES.txt +13 -0
  49. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant.egg-info/requires.txt +3 -0
  50. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant_web_server.py +1158 -727
  51. {cfquant-0.2.55 → cfquant-0.2.57}/pyproject.toml +2 -1
  52. {cfquant-0.2.55 → cfquant-0.2.57}/qmt_scripts/CFQUANT_LITE.py +102 -15
  53. {cfquant-0.2.55 → cfquant-0.2.57}/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
  54. {cfquant-0.2.55 → cfquant-0.2.57}/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
  55. {cfquant-0.2.55 → cfquant-0.2.57}/requirements.txt +2 -0
  56. {cfquant-0.2.55 → cfquant-0.2.57}/web_dashboard/app.js +264 -75
  57. {cfquant-0.2.55 → cfquant-0.2.57}/web_dashboard/index.html +82 -27
  58. cfquant-0.2.57/web_dashboard/remote-management.js +52 -0
  59. {cfquant-0.2.55 → cfquant-0.2.57}/web_dashboard/styles.css +39 -6
  60. cfquant-0.2.55/cfquant/tests/25_/350/264/246/346/210/267/347/274/223/345/255/230/351/241/265/351/235/242/346/265/213/350/257/225.py +0 -88
  61. cfquant-0.2.55/cfquant/tests/test_qmt_component_login.py +0 -165
  62. cfquant-0.2.55/cfquant/tests/test_qmt_dependencies.py +0 -55
  63. {cfquant-0.2.55 → cfquant-0.2.57}/LICENSE +0 -0
  64. {cfquant-0.2.55 → cfquant-0.2.57}/LTtx/__init__.py +0 -0
  65. {cfquant-0.2.55 → cfquant-0.2.57}/LTtx/tx/Config.txt +0 -0
  66. {cfquant-0.2.55 → cfquant-0.2.57}/LTtx/tx/LTtx_server.py +0 -0
  67. {cfquant-0.2.55 → cfquant-0.2.57}/LTtx/tx/__init__.py +0 -0
  68. {cfquant-0.2.55 → cfquant-0.2.57}/LTtx/tx/tx.py +0 -0
  69. {cfquant-0.2.55 → cfquant-0.2.57}/MANIFEST.in +0 -0
  70. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/__init__.py +0 -0
  71. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/account_routing.py +0 -0
  72. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/build_info.py +0 -0
  73. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/callback_dispatcher.py +0 -0
  74. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/channels.py +0 -0
  75. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/cli.py +0 -0
  76. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/config.py +0 -0
  77. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/diagnostics.py +0 -0
  78. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/flow_control.py +0 -0
  79. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/hub.py +0 -0
  80. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/level2.py +0 -0
  81. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/log_management.py +0 -0
  82. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/logging_i18n.py +0 -0
  83. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/order_identity.py +0 -0
  84. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/order_meta.py +0 -0
  85. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/pipe_bridge.py +0 -0
  86. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/pipe_hub.py +0 -0
  87. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/pipe_transport.py +0 -0
  88. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/qmt_binding_settings.py +0 -0
  89. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/qmt_bridge.py +0 -0
  90. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/qmt_credentials.py +0 -0
  91. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/qmt_strategy_package.py +0 -0
  92. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/qmt_strategy_runtime.py +0 -0
  93. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/quote_history.py +0 -0
  94. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/runtime_report.py +0 -0
  95. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/stock_connect.py +0 -0
  96. {cfquant-0.2.55 → cfquant-0.2.57}/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
  97. {cfquant-0.2.55 → cfquant-0.2.57}/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
  98. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/12_/350/256/242/345/215/225/345/244/207/346/263/250/346/265/213/350/257/225.py" +0 -0
  99. {cfquant-0.2.55 → cfquant-0.2.57}/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
  100. {cfquant-0.2.55 → cfquant-0.2.57}/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
  101. {cfquant-0.2.55 → cfquant-0.2.57}/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
  102. {cfquant-0.2.55 → cfquant-0.2.57}/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
  103. {cfquant-0.2.55 → cfquant-0.2.57}/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
  104. {cfquant-0.2.55 → cfquant-0.2.57}/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
  105. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/1_/350/241/214/346/203/205/346/216/245/346/224/266/346/265/213/350/257/225.py" +0 -0
  106. {cfquant-0.2.55 → cfquant-0.2.57}/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
  107. {cfquant-0.2.55 → cfquant-0.2.57}/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
  108. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/22_/350/277/236/346/216/245/350/257/212/346/226/255/346/265/213/350/257/225.py" +0 -0
  109. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/23_/347/244/272/344/276/213/351/205/215/347/275/256/346/265/213/350/257/225.py" +0 -0
  110. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/26_/345/205/250/346/216/250/350/256/242/351/230/205/346/265/213/350/257/225.py" +0 -0
  111. {cfquant-0.2.55 → cfquant-0.2.57}/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
  112. {cfquant-0.2.55 → cfquant-0.2.57}/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
  113. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/29_Level2/350/241/214/346/203/205/346/265/213/350/257/225.py" +0 -0
  114. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/2_/346/225/260/346/215/256/350/216/267/345/217/226/346/265/213/350/257/225.py" +0 -0
  115. {cfquant-0.2.55 → cfquant-0.2.57}/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
  116. {cfquant-0.2.55 → cfquant-0.2.57}/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
  117. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/32_/345/274/202/346/255/245/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  118. {cfquant-0.2.55 → cfquant-0.2.57}/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
  119. {cfquant-0.2.55 → cfquant-0.2.57}/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
  120. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/3_/346/225/260/346/215/256/344/270/213/350/275/275/346/265/213/350/257/225.py" +0 -0
  121. {cfquant-0.2.55 → cfquant-0.2.57}/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
  122. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/5_/347/234/237/345/256/236/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  123. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/6_/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
  124. {cfquant-0.2.55 → cfquant-0.2.57}/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
  125. {cfquant-0.2.55 → cfquant-0.2.57}/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
  126. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/9_/345/217/257/346/222/244/350/256/242/345/215/225/346/265/213/350/257/225.py" +0 -0
  127. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/_helpers.py +0 -0
  128. {cfquant-0.2.55 → cfquant-0.2.57}/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
  129. {cfquant-0.2.55 → cfquant-0.2.57}/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
  130. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/readme.md +0 -0
  131. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_advanced_parity.py +0 -0
  132. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_api_tester_browser.py +0 -0
  133. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_api_tester_web.py +0 -0
  134. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
  135. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_binding_runtime_browser.py +0 -0
  136. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_callback_requests.py +0 -0
  137. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_cftrader_web.py +0 -0
  138. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_credit_compat.py +0 -0
  139. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_dataframe_encoding.py +0 -0
  140. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_json_serialization.py +0 -0
  141. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_latency_flow_control.py +0 -0
  142. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_lite_account_routing.py +0 -0
  143. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_lite_deployment_paths.py +0 -0
  144. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_lite_parity.py +0 -0
  145. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_lite_qmt_dispatch.py +0 -0
  146. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_local_trading_calendar.py +0 -0
  147. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_log_management.py +0 -0
  148. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_management.py +0 -0
  149. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_management_batch.py +0 -0
  150. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_market_data_fallback.py +0 -0
  151. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_order_identity.py +0 -0
  152. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_order_time_callback.py +0 -0
  153. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_project_updater.py +0 -0
  154. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_python_api_reference.py +0 -0
  155. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_qmt_binding_settings.py +0 -0
  156. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_quote_latency.py +0 -0
  157. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_quote_latency_encoding.py +0 -0
  158. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
  159. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_startup_recovery.py +0 -0
  160. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_stock_connect.py +0 -0
  161. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_stock_connect_browser.py +0 -0
  162. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_table_conversion.py +0 -0
  163. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_trade_strategy_callback.py +0 -0
  164. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_trading_dates.py +0 -0
  165. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_tx_auto_import.py +0 -0
  166. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tests/test_tx_shutdown.py +0 -0
  167. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/trading_calendar.py +0 -0
  168. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/tx.py +0 -0
  169. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/xtconstant.py +0 -0
  170. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/xtdata.py +0 -0
  171. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant/xttype.py +0 -0
  172. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant.egg-info/dependency_links.txt +0 -0
  173. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant.egg-info/entry_points.txt +0 -0
  174. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant.egg-info/top_level.txt +0 -0
  175. {cfquant-0.2.55 → cfquant-0.2.57}/cfquant_pipe_hub.py +0 -0
  176. {cfquant-0.2.55 → cfquant-0.2.57}/qmt_scripts/CFQUANT.py +0 -0
  177. {cfquant-0.2.55 → cfquant-0.2.57}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +0 -0
  178. {cfquant-0.2.55 → cfquant-0.2.57}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
  179. {cfquant-0.2.55 → cfquant-0.2.57}/qmt_scripts/__init__.py +0 -0
  180. {cfquant-0.2.55 → cfquant-0.2.57}/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
  181. {cfquant-0.2.55 → cfquant-0.2.57}/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
  182. {cfquant-0.2.55 → cfquant-0.2.57}/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
  183. {cfquant-0.2.55 → cfquant-0.2.57}/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
  184. {cfquant-0.2.55 → cfquant-0.2.57}/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
  185. {cfquant-0.2.55 → cfquant-0.2.57}/setup.cfg +0 -0
  186. {cfquant-0.2.55 → cfquant-0.2.57}/web_dashboard/__init__.py +0 -0
  187. {cfquant-0.2.55 → cfquant-0.2.57}/web_dashboard/api-tester.js +0 -0
  188. {cfquant-0.2.55 → cfquant-0.2.57}/web_dashboard/avatars/amber-pulse.svg +0 -0
  189. {cfquant-0.2.55 → cfquant-0.2.57}/web_dashboard/avatars/copper-grid.svg +0 -0
  190. {cfquant-0.2.55 → cfquant-0.2.57}/web_dashboard/avatars/market-blue.svg +0 -0
  191. {cfquant-0.2.55 → cfquant-0.2.57}/web_dashboard/avatars/rose-circuit.svg +0 -0
  192. {cfquant-0.2.55 → cfquant-0.2.57}/web_dashboard/avatars/signal-green.svg +0 -0
  193. {cfquant-0.2.55 → cfquant-0.2.57}/web_dashboard/avatars/slate-wave.svg +0 -0
  194. {cfquant-0.2.55 → cfquant-0.2.57}/web_dashboard/avatars/teal-orbit.svg +0 -0
  195. {cfquant-0.2.55 → cfquant-0.2.57}/web_dashboard/avatars/violet-node.svg +0 -0
  196. {cfquant-0.2.55 → cfquant-0.2.57}/web_dashboard/cftrader-api-data.js +0 -0
  197. {cfquant-0.2.55 → cfquant-0.2.57}/web_dashboard/docs.js +0 -0
  198. {cfquant-0.2.55 → cfquant-0.2.57}/web_dashboard/favicon.svg +0 -0
  199. {cfquant-0.2.55 → cfquant-0.2.57}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
  200. {cfquant-0.2.55 → cfquant-0.2.57}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
  201. {cfquant-0.2.55 → cfquant-0.2.57}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
  202. {cfquant-0.2.55 → cfquant-0.2.57}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
  203. {cfquant-0.2.55 → cfquant-0.2.57}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
  204. {cfquant-0.2.55 → cfquant-0.2.57}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
  205. {cfquant-0.2.55 → cfquant-0.2.57}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
  206. {cfquant-0.2.55 → cfquant-0.2.57}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
  207. {cfquant-0.2.55 → cfquant-0.2.57}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
  208. {cfquant-0.2.55 → cfquant-0.2.57}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
  209. {cfquant-0.2.55 → cfquant-0.2.57}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
  210. {cfquant-0.2.55 → cfquant-0.2.57}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
  211. {cfquant-0.2.55 → cfquant-0.2.57}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
  212. {cfquant-0.2.55 → cfquant-0.2.57}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
  213. {cfquant-0.2.55 → cfquant-0.2.57}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
  214. {cfquant-0.2.55 → cfquant-0.2.57}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
  215. {cfquant-0.2.55 → cfquant-0.2.57}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
  216. {cfquant-0.2.55 → cfquant-0.2.57}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
  217. {cfquant-0.2.55 → cfquant-0.2.57}/web_dashboard/python-api-data.js +0 -0
  218. {cfquant-0.2.55 → cfquant-0.2.57}/web_dashboard/python-api.js +0 -0
  219. {cfquant-0.2.55 → cfquant-0.2.57}/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.55
3
+ Version: 0.2.57
4
4
  Summary: QMT local bridge and xtquant-compatible Python client.
5
5
  License: MIT
6
6
  Requires-Python: >=3.8
@@ -9,6 +9,7 @@ License-File: LICENSE
9
9
  Requires-Dist: pandas>=1.0
10
10
  Requires-Dist: packaging>=20
11
11
  Requires-Dist: psutil>=5.8
12
+ Requires-Dist: tomli>=1.1.0; python_version < "3.11"
12
13
  Requires-Dist: pycryptodome>=3.20
13
14
  Requires-Dist: pytz
14
15
  Requires-Dist: tabulate
@@ -343,6 +344,8 @@ trader.disconnect()
343
344
 
344
345
  `XtQuantTrader`、`StockAccount`、常用行情查询和交易方法保持接近 `xtquant` 的调用方式,原有回调类通常可以继续使用。仍有部分 QMT 专有接口、字段和返回结构存在兼容边界,使用前请查看 [xtquant 原版接口适配清单](docs/xtquant原版接口适配清单.md) 和 [QMT 函数封装能力清单](docs/QMT函数封装能力清单.md)。
345
346
 
347
+ 账号订阅成功后,SDK 会在后台约每两秒重新确认订阅。用户程序保持运行时,QMT、cfquant Web 或通信服务重启后,待桥接策略恢复授权、通道可用,SDK 会自动恢复该账号的新回调;默认 Web 路由和直接 Pipe/LTtx 连接均适用。主动 `unsubscribe(account)` 后不会再恢复该账号,`stop()` / `disconnect()` 会停止后台恢复。此机制只恢复账号订阅,不补发停机期间的回调,也不重试委托或撤单。升级 SDK 后需先重启一次用户程序加载新代码,此后无需随 QMT 重启用户程序。
348
+
346
349
  推荐迁移顺序:**行情查询 → 资金/持仓 → 回调 → 模拟委托 → 模拟撤单 → 正式策略**。每一步都先在 Web 控制台接口测试和日志中确认,再进入下一步。
347
350
 
348
351
  ## 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
- 推荐迁移顺序:**行情查询 → 资金/持仓 → 回调 → 模拟委托 → 模拟撤单 → 正式策略**。每一步都先在 Web 控制台接口测试和日志中确认,再进入下一步。
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
 
@@ -16,6 +16,7 @@ else:
16
16
 
17
17
 
18
18
  PACKAGE_NAME = "cfquant"
19
+ TOMLI_REQUIREMENT = "tomli>=1.1.0"
19
20
 
20
21
 
21
22
  def _hidden_subprocess_kwargs():
@@ -118,6 +119,21 @@ def requirements_install_args(project_root, python_exe=None):
118
119
  return command
119
120
 
120
121
 
122
+ def tomli_install_args(project_root=None, python_exe=None):
123
+ """Build the bootstrap command needed before parsing TOML on old Python."""
124
+ command = [
125
+ python_exe or sys.executable,
126
+ "-m",
127
+ "pip",
128
+ "install",
129
+ "--disable-pip-version-check",
130
+ "--no-input",
131
+ ]
132
+ command.extend(pip_index_args())
133
+ command.append(TOMLI_REQUIREMENT)
134
+ return command
135
+
136
+
121
137
  def _output_tail(value, limit=6000):
122
138
  text = str(value or "")
123
139
  if len(text) <= int(limit):
@@ -478,7 +494,12 @@ def project_package_version(project_root):
478
494
  try:
479
495
  import tomllib
480
496
  except ImportError:
481
- from pip._vendor import tomli as tomllib
497
+ try:
498
+ import tomli as tomllib
499
+ except ImportError:
500
+ # Keep compatibility with pip versions that vendor tomli while
501
+ # preferring the project's explicit conditional dependency.
502
+ from pip._vendor import tomli as tomllib
482
503
  with (Path(project_root) / "pyproject.toml").open("rb") as stream:
483
504
  return str(tomllib.load(stream)["project"]["version"])
484
505
 
@@ -503,7 +524,24 @@ def ensure_cfquant_installed(project_root, python_exe=None):
503
524
  return 2
504
525
 
505
526
  python_exe = python_exe or sys.executable
506
- expected = project_package_version(project_root)
527
+ try:
528
+ expected = project_package_version(project_root)
529
+ except ImportError:
530
+ # project_package_version runs before the project itself is installed.
531
+ # Bootstrap tomli here so a clean Python 3.8-3.10 environment can still
532
+ # complete its first startup without requiring a manual pip command.
533
+ print("TOML parser unavailable; installing %s before startup check." % TOMLI_REQUIREMENT)
534
+ bootstrap = run(tomli_install_args(project_root, python_exe), project_root)
535
+ if bootstrap.returncode != 0:
536
+ print("[ERROR] Could not install %s; cannot read pyproject.toml." % TOMLI_REQUIREMENT,
537
+ file=sys.stderr)
538
+ return bootstrap.returncode
539
+ try:
540
+ expected = project_package_version(project_root)
541
+ except ImportError:
542
+ print("[ERROR] %s is still unavailable after bootstrap installation." % TOMLI_REQUIREMENT,
543
+ file=sys.stderr)
544
+ return 1
507
545
  installed = installed_package_version(python_exe)
508
546
  print("cfquant installed=%s project=%s" % (installed or "missing", expected))
509
547
  if installed == expected:
@@ -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
- batch_unknown(results, "Batch response unavailable; QMT may still execute the batch: %s" % error)
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
- batch_unknown(results, "Batch cancel response unavailable; QMT may still execute the batch: %s" % error)
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
- pass
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()
@@ -100,6 +100,7 @@ class RuntimeManager:
100
100
  self._process = None
101
101
  self.accounts = AccountManager(self)
102
102
  self.updates = UpdateManager(self)
103
+ self.remote = RemoteManager(self)
103
104
  # Local service requests should never be sent to an HTTP proxy.
104
105
  self._opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
105
106
 
@@ -337,6 +338,21 @@ class RuntimeManager:
337
338
  configure(host=info["lttx_host"], port=info["lttx_port"], transport="web_lttx")
338
339
 
339
340
 
341
+ class RemoteManager:
342
+ """Opt-in connection using a project API key created on the official site."""
343
+
344
+ def __init__(self, runtime):
345
+ self.runtime = runtime
346
+
347
+ def status(self):
348
+ return self.runtime._request("GET", "/api/remote-management")
349
+
350
+ def configure(self, *, enabled=True, api_key=None, permissions=None):
351
+ body = {"enabled": bool(enabled)}
352
+ body.update({k: v for k, v in {"api_key": api_key, "permissions": permissions}.items() if v is not None})
353
+ return self.runtime._request("POST", "/api/remote-management", body)
354
+
355
+
340
356
  class AccountManager:
341
357
  def __init__(self, runtime):
342
358
  self.runtime = runtime
@@ -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
- self._rx_conn = connect_pipe(self.pipe_name, timeout_ms=self.connect_timeout_ms)
52
- self._rx_conn.write_frame(dumps_pipe_message({
53
- "type": "hello",
54
- "role": "api_rx",
55
- "client_id": self.client_id,
56
- }))
57
- self._tx_conn = connect_pipe(self.pipe_name, timeout_ms=self.connect_timeout_ms)
58
- self._tx_conn.write_frame(dumps_pipe_message({
59
- "type": "hello",
60
- "role": "api_tx",
61
- "client_id": self.client_id,
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
- return {
219
- "type": type(error).__name__,
220
- "message": str(error),
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):
@@ -115,10 +115,19 @@ function noticeKind(rows) {
115
115
  }
116
116
 
117
117
  function submit(root) {
118
+ const rows = walk(root);
119
+ const edits = rows.filter(row => row.echo_mode === 2 && row.visible && row.enabled);
120
+ // Check on the GUI thread as well: an update can disable the button after
121
+ // Python has inspected it. A declined submission is safe to retry later.
122
+ if (edits.length !== 1 || !hasText(ptr(edits[0].address)) ||
123
+ !rows.some(row => row.visible && row.enabled && /^登\s*录(?:[((]\d+[))])?$/.test(row.label || ''))) {
124
+ return {submitted: false};
125
+ }
118
126
  passwordEditor(root);
119
127
  const login = fn(Process.getModuleByName('LoginDialog.dll'),
120
128
  '?slot_loginClicked@CLoginDialog@TC@@QEAAXXZ', 'void', ['pointer']);
121
129
  login(root);
130
+ return {submitted: true};
122
131
  }
123
132
 
124
133
  function loginIdentity(root) {
@@ -190,7 +199,7 @@ rpc.exports = {
190
199
  const edits = rows.filter(row => row.echo_mode === 2 && row.visible);
191
200
  if (edits.length > 1) throw new Error('Multiple password editors are visible');
192
201
  return {is_login: true, has_password: edits.length === 1 && hasText(ptr(edits[0].address)),
193
- busy: edits.length === 0, login_identity: loginIdentity(root), notice: noticeKind(rows),
202
+ busy: edits.length === 0 || !edits[0].enabled, login_identity: loginIdentity(root), notice: noticeKind(rows),
194
203
  login_enabled: rows.some(row => row.visible && row.enabled && /^登\s*录(?:[((]\d+[))])?$/.test(row.label || ''))};
195
204
  });
196
205
  },
@@ -210,7 +219,7 @@ rpc.exports = {
210
219
  return {acknowledged: true};
211
220
  });
212
221
  },
213
- submit(hwnd) { return onGui(hwnd, () => { submit(getRoot(hwnd)); return {submitted: true}; }); },
222
+ submit(hwnd) { return onGui(hwnd, () => submit(getRoot(hwnd))); },
214
223
  close(hwnd) { return onGui(hwnd, () => ({closed: closeWidget(getRoot(hwnd))})); },
215
224
  clicklabel(hwnd, text) {
216
225
  return onGui(hwnd, () => {
@@ -16,6 +16,10 @@ class QmtNotReady(RuntimeError):
16
16
  pass
17
17
 
18
18
 
19
+ class QmtProcessExited(RuntimeError):
20
+ pass
21
+
22
+
19
23
  def verify_environment(directory):
20
24
  """Validate dependencies and native ABI before stopping a running terminal."""
21
25
  try:
@@ -37,8 +41,13 @@ def verify_environment(directory):
37
41
  raise RuntimeError("QMT 组件自动登录需要 64 位客户端")
38
42
 
39
43
 
40
- def auto_login(backend, password_provider):
41
- """Replace existing password contents and submit the supplied credential once."""
44
+ def auto_login(backend, password_provider, *, retry_not_ready=False, login_account=None,
45
+ should_cancel=lambda: False, on_wait=lambda phase: None):
46
+ """Wait through startup/update when managed, and submit the credential once.
47
+
48
+ Readiness retries never force a disabled button. After submission, a timeout
49
+ only renews observation: it is not evidence that another submission is safe.
50
+ """
42
51
  attempts = []
43
52
  password_requested = False
44
53
 
@@ -46,28 +55,59 @@ def auto_login(backend, password_provider):
46
55
  return {"outcome": outcome, "login_succeeded": outcome in ("logged_in", "already_logged_in"),
47
56
  "attempts": attempts, "password_requested": password_requested}
48
57
 
49
- if backend.already_logged_in():
50
- return result("already_logged_in")
51
- state = backend.state()
52
- if state.get("busy"):
53
- return result("login_not_ready")
54
- if state.get("notice") in ("challenge", "network_error"):
55
- return result(state["notice"])
56
- if state.get("notice") == "credential_error":
57
- backend.acknowledge_failure()
58
- password_requested = True
59
- password = password_provider()
60
- if not password:
61
- return result("password_required")
62
- try:
63
- backend.fill(password)
64
- finally:
65
- password = None
66
- if not backend.state()["login_enabled"]:
67
- return result("login_not_ready")
68
- backend.submit()
69
- attempts.append("provided")
70
- return result(backend.wait_outcome())
58
+ while True:
59
+ if should_cancel():
60
+ return result("cancelled")
61
+ if backend.already_logged_in():
62
+ if login_account is not None:
63
+ backend.select_account(login_account)
64
+ return result("already_logged_in")
65
+ state = backend.state()
66
+ if state.get("notice") in ("challenge", "network_error"):
67
+ return result(state["notice"])
68
+ if not state.get("busy"):
69
+ if not password_requested or not state.get("has_password"):
70
+ if not password_requested:
71
+ if state.get("notice") == "credential_error":
72
+ backend.acknowledge_failure()
73
+ if login_account is not None:
74
+ backend.select_account(login_account, replace=True)
75
+ if should_cancel():
76
+ return result("cancelled")
77
+ password_requested = True
78
+ password = password_provider()
79
+ if not password:
80
+ return result("password_required")
81
+ try:
82
+ backend.fill(password)
83
+ finally:
84
+ password = None
85
+ state = backend.state()
86
+ if state.get("login_enabled") and not state.get("busy"):
87
+ if should_cancel():
88
+ return result("cancelled")
89
+ try:
90
+ backend.submit()
91
+ except QmtNotReady:
92
+ # The button can become disabled between status and submit.
93
+ pass
94
+ else:
95
+ attempts.append("provided")
96
+ break
97
+ if not retry_not_ready:
98
+ return result("login_not_ready")
99
+ on_wait("waiting_ready")
100
+ time.sleep(0.5)
101
+
102
+ on_wait("logging_in")
103
+ while True:
104
+ if should_cancel():
105
+ return result("cancelled")
106
+ outcome = backend.wait_outcome()
107
+ if outcome != "timeout" or not retry_not_ready:
108
+ return result(outcome)
109
+ on_wait("logging_in")
110
+ time.sleep(0.5)
71
111
 
72
112
 
73
113
  def select_binding(config_path, name):
@@ -104,6 +144,7 @@ class QtLoginBackend:
104
144
  self.session = None
105
145
  self.mutex = None
106
146
  self.messages = []
147
+ self.should_cancel = lambda: False
107
148
  self.foreground_before = win32gui.GetForegroundWindow()
108
149
  expected = (directory / "XtItClient.exe").resolve()
109
150
  matches = []
@@ -114,6 +155,8 @@ class QtLoginBackend:
114
155
  except (psutil.NoSuchProcess, psutil.AccessDenied):
115
156
  continue
116
157
  if not matches:
158
+ if expected_pid is not None and not psutil.pid_exists(expected_pid):
159
+ raise QmtProcessExited("QMT process exited")
117
160
  raise QmtNotReady("QMT process is not ready")
118
161
  if len(matches) != 1:
119
162
  raise RuntimeError("Expected exactly one running QMT for this binding directory")
@@ -167,11 +210,18 @@ class QtLoginBackend:
167
210
  return result
168
211
 
169
212
  def already_logged_in(self):
213
+ if self.session.is_detached:
214
+ raise QmtProcessExited("QMT automation session detached")
215
+ if not self.existing_main or self.hwnd is not None:
216
+ for hwnd in self.windows():
217
+ if self.api.status(str(hwnd))["result"].get("is_main"):
218
+ self.existing_main, self.main_hwnd, self.hwnd = True, hwnd, None
219
+ break
170
220
  return self.existing_main and self.hwnd is None
171
221
 
172
222
  def state(self):
173
223
  state = self.api.status(str(self.hwnd))["result"]
174
- if not state.get("is_login") or not self.identity or state.get("login_identity") != self.identity:
224
+ if not state.get("is_login") or state.get("login_identity") != self.identity:
175
225
  raise RuntimeError("Login window or selected account changed; stopping")
176
226
  state.pop("login_identity", None)
177
227
  for hwnd in self.windows():
@@ -197,8 +247,9 @@ class QtLoginBackend:
197
247
  def submit(self):
198
248
  state = self.state()
199
249
  if not state["has_password"] or not state["login_enabled"]:
200
- raise RuntimeError("Login is not ready")
201
- self.api.submit(str(self.hwnd))
250
+ raise QmtNotReady("Login is not ready")
251
+ if not self.api.submit(str(self.hwnd))["result"]["submitted"]:
252
+ raise QmtNotReady("Login is not ready")
202
253
 
203
254
  def fill(self, password):
204
255
  self.state()
@@ -207,6 +258,10 @@ class QtLoginBackend:
207
258
  def wait_outcome(self):
208
259
  deadline = time.monotonic() + self.timeout
209
260
  while time.monotonic() < deadline:
261
+ if self.should_cancel():
262
+ return "cancelled"
263
+ if self.session.is_detached:
264
+ raise QmtProcessExited("QMT automation session detached")
210
265
  windows = self.windows()
211
266
  for hwnd in windows:
212
267
  caption = self.gui.GetWindowText(hwnd)
@@ -291,28 +346,41 @@ def main():
291
346
 
292
347
 
293
348
  def login_running_qmt(directory, login_account="", password_provider=lambda: "", expected_pid=None,
294
- startup_timeout=60, timeout=40):
295
- """Wait for a freshly launched QMT, then execute one bounded login flow."""
296
- deadline = time.monotonic() + startup_timeout
349
+ startup_timeout=60, timeout=40, *, should_cancel=lambda: False,
350
+ on_wait=lambda phase: None, before_poll=lambda: None):
351
+ """Wait for QMT startup/updates and observe login until success or rejection.
352
+
353
+ The web worker uses startup_timeout=None to wait while its process is alive.
354
+ Cancellation stops waiting when that worker is disabled or superseded.
355
+ """
356
+ cancelled = should_cancel
357
+ def should_cancel():
358
+ if cancelled():
359
+ return True
360
+ before_poll()
361
+ return cancelled()
362
+
363
+ deadline = None if startup_timeout is None else time.monotonic() + startup_timeout
364
+ backend = None
297
365
  while True:
366
+ if should_cancel():
367
+ return {"outcome": "cancelled", "login_succeeded": False, "attempts": []}
298
368
  try:
299
369
  backend = QtLoginBackend(Path(directory), timeout, expected_pid=expected_pid)
300
370
  break
371
+ except QmtProcessExited:
372
+ return {"outcome": "process_exited", "login_succeeded": False, "attempts": []}
301
373
  except QmtNotReady:
302
- if time.monotonic() >= deadline:
374
+ if deadline is not None and time.monotonic() >= deadline:
303
375
  return {"outcome": "startup_timeout", "login_succeeded": False, "attempts": []}
376
+ on_wait("waiting_ready")
304
377
  time.sleep(0.5)
305
378
  try:
306
- if not backend.already_logged_in() and backend.state().get("busy"):
307
- outcome = backend.wait_outcome()
308
- if outcome == "logged_in":
309
- # Reopen only the automation session to verify the resulting user.
310
- backend.close()
311
- backend = QtLoginBackend(Path(directory), timeout, expected_pid=expected_pid)
312
- elif outcome != "credential_error":
313
- return {"outcome": outcome, "login_succeeded": False, "attempts": []}
314
- backend.select_account(login_account, replace=True)
315
- return auto_login(backend, password_provider)
379
+ backend.should_cancel = should_cancel
380
+ return auto_login(backend, password_provider, retry_not_ready=True, login_account=login_account,
381
+ should_cancel=should_cancel, on_wait=on_wait)
382
+ except QmtProcessExited:
383
+ return {"outcome": "process_exited", "login_succeeded": False, "attempts": []}
316
384
  finally:
317
385
  backend.close()
318
386