cfquant 0.2.54__tar.gz → 0.2.56__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. {cfquant-0.2.54 → cfquant-0.2.56}/PKG-INFO +3 -1
  2. {cfquant-0.2.54 → cfquant-0.2.56}/README.md +5 -3
  3. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/_editable_install.py +7 -0
  4. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/batch_orders.py +15 -0
  5. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/cftrader.py +21 -3
  6. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/client.py +13 -3
  7. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/normal_bridge.py +24 -0
  8. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/pipe_client.py +20 -13
  9. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/protocol.py +5 -4
  10. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/qmt_component_login.py +21 -10
  11. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/qmt_credentials.py +8 -2
  12. cfquant-0.2.56/cfquant/qmt_dependencies.py +158 -0
  13. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/qmt_pipe_runtime.py +28 -5
  14. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/qmt_strategy_deploy.py +66 -35
  15. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/rpc_timing.py +15 -0
  16. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/24_/350/264/246/346/210/267/347/274/223/345/255/230/346/214/211/351/234/200/345/210/267/346/226/260/346/265/213/350/257/225.py +150 -0
  17. cfquant-0.2.56/cfquant/tests/25_/350/264/246/346/210/267/347/274/223/345/255/230/351/241/265/351/235/242/346/265/213/350/257/225.py +170 -0
  18. cfquant-0.2.56/cfquant/tests/test_account_query_timing.py +131 -0
  19. cfquant-0.2.56/cfquant/tests/test_binding_live_refresh_browser.py +95 -0
  20. cfquant-0.2.56/cfquant/tests/test_callback_recovery.py +287 -0
  21. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_cftrader.py +17 -1
  22. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_cftrader_tutorial.py +5 -0
  23. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_market_timing.py +1 -1
  24. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_qmt_auto_login_browser.py +24 -0
  25. cfquant-0.2.56/cfquant/tests/test_qmt_dependencies.py +107 -0
  26. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_qmt_fileless.py +129 -3
  27. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_qmt_strategy_browser.py +14 -6
  28. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_qmt_strategy_deploy.py +29 -0
  29. cfquant-0.2.56/cfquant/tests/test_remote_version_check.py +72 -0
  30. cfquant-0.2.56/cfquant/tests/test_trader_subscription_recovery.py +345 -0
  31. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_tutorial_reader.py +3 -0
  32. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tx_trade_bridge.py +36 -12
  33. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/version.py +2 -2
  34. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/xttrader.py +201 -43
  35. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant.egg-info/PKG-INFO +3 -1
  36. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant.egg-info/SOURCES.txt +7 -0
  37. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant_web_server.py +494 -246
  38. {cfquant-0.2.54 → cfquant-0.2.56}/pyproject.toml +1 -1
  39. {cfquant-0.2.54 → cfquant-0.2.56}/qmt_scripts/CFQUANT_LITE.py +102 -15
  40. {cfquant-0.2.54 → cfquant-0.2.56}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_LITE_SH.py +102 -15
  41. {cfquant-0.2.54 → cfquant-0.2.56}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_LITE_SZ.py +102 -15
  42. {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/app.js +276 -53
  43. {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/index.html +20 -5
  44. {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/styles.css +57 -6
  45. cfquant-0.2.54/cfquant/tests/25_/350/264/246/346/210/267/347/274/223/345/255/230/351/241/265/351/235/242/346/265/213/350/257/225.py +0 -88
  46. {cfquant-0.2.54 → cfquant-0.2.56}/LICENSE +0 -0
  47. {cfquant-0.2.54 → cfquant-0.2.56}/LTtx/__init__.py +0 -0
  48. {cfquant-0.2.54 → cfquant-0.2.56}/LTtx/tx/Config.txt +0 -0
  49. {cfquant-0.2.54 → cfquant-0.2.56}/LTtx/tx/LTtx_server.py +0 -0
  50. {cfquant-0.2.54 → cfquant-0.2.56}/LTtx/tx/__init__.py +0 -0
  51. {cfquant-0.2.54 → cfquant-0.2.56}/LTtx/tx/tx.py +0 -0
  52. {cfquant-0.2.54 → cfquant-0.2.56}/MANIFEST.in +0 -0
  53. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/__init__.py +0 -0
  54. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/account_routing.py +0 -0
  55. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/build_info.py +0 -0
  56. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/callback_dispatcher.py +0 -0
  57. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/channels.py +0 -0
  58. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/cli.py +0 -0
  59. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/config.py +0 -0
  60. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/diagnostics.py +0 -0
  61. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/flow_control.py +0 -0
  62. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/hub.py +0 -0
  63. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/level2.py +0 -0
  64. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/log_management.py +0 -0
  65. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/logging_i18n.py +0 -0
  66. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/management.py +0 -0
  67. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/order_identity.py +0 -0
  68. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/order_meta.py +0 -0
  69. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/pipe_bridge.py +0 -0
  70. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/pipe_hub.py +0 -0
  71. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/pipe_transport.py +0 -0
  72. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/qmt_binding_settings.py +0 -0
  73. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/qmt_bridge.py +0 -0
  74. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/qmt_component_agent.js +0 -0
  75. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/qmt_strategy_package.py +0 -0
  76. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/qmt_strategy_runtime.py +0 -0
  77. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/quote_history.py +0 -0
  78. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/runtime_report.py +0 -0
  79. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/stock_connect.py +0 -0
  80. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/10_/345/221/275/344/273/244/350/241/214/346/216/245/345/217/243/346/265/213/350/257/225.py" +0 -0
  81. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/11_/346/231/256/351/200/232/346/241/245/346/216/245/346/264/276/345/217/221/346/265/213/350/257/225.py" +0 -0
  82. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/12_/350/256/242/345/215/225/345/244/207/346/263/250/346/265/213/350/257/225.py" +0 -0
  83. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/13_/347/256/241/351/201/223/344/270/255/345/277/203/350/277/236/346/216/245/346/265/213/350/257/225.py" +0 -0
  84. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/14_/350/277/220/350/241/214/346/227/266/346/212/245/345/221/212/346/265/213/350/257/225.py" +0 -0
  85. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/15_/347/275/221/351/241/265/347/212/266/346/200/201/347/274/223/345/255/230/346/265/213/350/257/225.py" +0 -0
  86. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/16_/345/220/257/345/212/250/345/256/211/350/243/205/346/265/213/350/257/225.py" +0 -0
  87. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/17_/345/244/247QMT/346/216/245/345/217/243/351/200/202/351/205/215/346/265/213/350/257/225.py" +0 -0
  88. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/18_/351/253/230/347/272/247/346/250/241/345/274/217/345/256/236/346/234/272/350/201/224/350/260/203.py" +0 -0
  89. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/19_/345/256/236/346/234/272/350/201/224/350/260/203/345/210/244/345/256/232/346/265/213/350/257/225.py" +0 -0
  90. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/1_/350/241/214/346/203/205/346/216/245/346/224/266/346/265/213/350/257/225.py" +0 -0
  91. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/20_/350/201/224/350/260/203/351/227/256/351/242/230/344/277/256/345/244/215/346/265/213/350/257/225.py" +0 -0
  92. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/21_/345/215/225/350/202/241/350/241/214/346/203/205/350/256/242/351/230/205/346/265/213/350/257/225.py" +0 -0
  93. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/22_/350/277/236/346/216/245/350/257/212/346/226/255/346/265/213/350/257/225.py" +0 -0
  94. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/23_/347/244/272/344/276/213/351/205/215/347/275/256/346/265/213/350/257/225.py" +0 -0
  95. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/26_/345/205/250/346/216/250/350/256/242/351/230/205/346/265/213/350/257/225.py" +0 -0
  96. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/27_/345/205/250/346/216/250/350/256/242/351/230/205/351/241/265/351/235/242/346/265/213/350/257/225.py" +0 -0
  97. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/28_Level2/346/216/245/345/217/243/351/200/202/351/205/215/346/265/213/350/257/225.py" +0 -0
  98. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/29_Level2/350/241/214/346/203/205/346/265/213/350/257/225.py" +0 -0
  99. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/2_/346/225/260/346/215/256/350/216/267/345/217/226/346/265/213/350/257/225.py" +0 -0
  100. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/30_cftrader/346/211/271/351/207/217/344/270/213/345/215/225/346/200/247/350/203/275/345/237/272/345/207/206.py" +0 -0
  101. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/31_cftrader/346/250/241/346/213/237/350/264/246/345/217/267/346/211/271/351/207/217/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  102. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/32_/345/274/202/346/255/245/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  103. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/33_/345/220/214/346/255/245/344/270/213/345/215/225/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
  104. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/34_/345/205/250/346/216/250/345/206/205/351/203/250/345/273/266/350/277/237/346/265/213/350/257/225.py" +0 -0
  105. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/3_/346/225/260/346/215/256/344/270/213/350/275/275/346/265/213/350/257/225.py" +0 -0
  106. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/4_/344/272/244/346/230/223/345/247/224/346/211/230/346/237/245/350/257/242/346/265/213/350/257/225.py" +0 -0
  107. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/5_/347/234/237/345/256/236/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  108. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/6_/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
  109. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/7_/345/220/214/346/255/245/345/274/202/346/255/245/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  110. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/8_/346/210/220/344/272/244/350/256/242/345/215/225/345/222/214/345/247/224/346/211/230/350/256/242/345/215/225/346/237/245/350/257/242/346/265/213/350/257/225.py" +0 -0
  111. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/9_/345/217/257/346/222/244/350/256/242/345/215/225/346/265/213/350/257/225.py" +0 -0
  112. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/_helpers.py +0 -0
  113. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/log/cftrader/346/250/241/346/213/237/350/264/246/345/217/267/346/211/271/351/207/217/344/270/213/345/215/225/346/265/213/350/257/225_20260911_024617.md" +0 -0
  114. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/log/cftrader/346/250/241/346/213/237/350/264/246/345/217/267/346/211/271/351/207/217/344/270/213/345/215/225/346/265/213/350/257/225_20260921_175732.md" +0 -0
  115. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/readme.md +0 -0
  116. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_advanced_parity.py +0 -0
  117. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_api_tester_browser.py +0 -0
  118. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_api_tester_web.py +0 -0
  119. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
  120. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_binding_runtime_browser.py +0 -0
  121. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_callback_requests.py +0 -0
  122. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_cftrader_web.py +0 -0
  123. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_credit_compat.py +0 -0
  124. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_dataframe_encoding.py +0 -0
  125. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_json_serialization.py +0 -0
  126. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_latency_flow_control.py +0 -0
  127. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_lite_account_routing.py +0 -0
  128. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_lite_deployment_paths.py +0 -0
  129. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_lite_parity.py +0 -0
  130. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_lite_qmt_dispatch.py +0 -0
  131. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_local_trading_calendar.py +0 -0
  132. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_log_management.py +0 -0
  133. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_management.py +0 -0
  134. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_management_batch.py +0 -0
  135. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_market_data_fallback.py +0 -0
  136. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_order_identity.py +0 -0
  137. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_order_time_callback.py +0 -0
  138. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_project_updater.py +0 -0
  139. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_python_api_reference.py +0 -0
  140. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_qmt_auto_login_integration.py +0 -0
  141. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_qmt_binding_settings.py +0 -0
  142. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_qmt_component_login.py +0 -0
  143. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_qmt_strategy_web.py +0 -0
  144. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_quote_latency.py +0 -0
  145. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_quote_latency_encoding.py +0 -0
  146. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
  147. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_startup_recovery.py +0 -0
  148. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_stock_connect.py +0 -0
  149. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_stock_connect_browser.py +0 -0
  150. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_table_conversion.py +0 -0
  151. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_trade_strategy_callback.py +0 -0
  152. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_trading_dates.py +0 -0
  153. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_tx_auto_import.py +0 -0
  154. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tests/test_tx_shutdown.py +0 -0
  155. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/trading_calendar.py +0 -0
  156. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/tx.py +0 -0
  157. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/xtconstant.py +0 -0
  158. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/xtdata.py +0 -0
  159. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant/xttype.py +0 -0
  160. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant.egg-info/dependency_links.txt +0 -0
  161. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant.egg-info/entry_points.txt +0 -0
  162. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant.egg-info/requires.txt +0 -0
  163. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant.egg-info/top_level.txt +0 -0
  164. {cfquant-0.2.54 → cfquant-0.2.56}/cfquant_pipe_hub.py +0 -0
  165. {cfquant-0.2.54 → cfquant-0.2.56}/qmt_scripts/CFQUANT.py +0 -0
  166. {cfquant-0.2.54 → cfquant-0.2.56}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +0 -0
  167. {cfquant-0.2.54 → cfquant-0.2.56}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
  168. {cfquant-0.2.54 → cfquant-0.2.56}/qmt_scripts/__init__.py +0 -0
  169. {cfquant-0.2.54 → cfquant-0.2.56}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_CTYPE_ALL_LOWLAT_SH.py" +0 -0
  170. {cfquant-0.2.54 → cfquant-0.2.56}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_CTYPE_ALL_LOWLAT_SZ.py" +0 -0
  171. {cfquant-0.2.54 → cfquant-0.2.56}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_TRADE_LOWLAT_SH.py" +0 -0
  172. {cfquant-0.2.54 → cfquant-0.2.56}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_TRADE_LOWLAT_SZ.py" +0 -0
  173. {cfquant-0.2.54 → cfquant-0.2.56}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/readme.md" +0 -0
  174. {cfquant-0.2.54 → cfquant-0.2.56}/requirements.txt +0 -0
  175. {cfquant-0.2.54 → cfquant-0.2.56}/setup.cfg +0 -0
  176. {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/__init__.py +0 -0
  177. {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/api-tester.js +0 -0
  178. {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/avatars/amber-pulse.svg +0 -0
  179. {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/avatars/copper-grid.svg +0 -0
  180. {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/avatars/market-blue.svg +0 -0
  181. {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/avatars/rose-circuit.svg +0 -0
  182. {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/avatars/signal-green.svg +0 -0
  183. {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/avatars/slate-wave.svg +0 -0
  184. {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/avatars/teal-orbit.svg +0 -0
  185. {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/avatars/violet-node.svg +0 -0
  186. {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/cftrader-api-data.js +0 -0
  187. {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/docs.js +0 -0
  188. {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/favicon.svg +0 -0
  189. {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
  190. {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
  191. {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
  192. {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
  193. {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
  194. {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
  195. {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
  196. {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
  197. {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
  198. {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
  199. {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
  200. {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
  201. {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
  202. {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
  203. {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
  204. {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
  205. {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
  206. {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
  207. {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/python-api-data.js +0 -0
  208. {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/python-api.js +0 -0
  209. {cfquant-0.2.54 → cfquant-0.2.56}/web_dashboard/vendor/mermaid.min.js +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cfquant
3
- Version: 0.2.54
3
+ Version: 0.2.56
4
4
  Summary: QMT local bridge and xtquant-compatible Python client.
5
5
  License: MIT
6
6
  Requires-Python: >=3.8
@@ -343,6 +343,8 @@ trader.disconnect()
343
343
 
344
344
  `XtQuantTrader`、`StockAccount`、常用行情查询和交易方法保持接近 `xtquant` 的调用方式,原有回调类通常可以继续使用。仍有部分 QMT 专有接口、字段和返回结构存在兼容边界,使用前请查看 [xtquant 原版接口适配清单](docs/xtquant原版接口适配清单.md) 和 [QMT 函数封装能力清单](docs/QMT函数封装能力清单.md)。
345
345
 
346
+ 账号订阅成功后,SDK 会在后台约每两秒重新确认订阅。用户程序保持运行时,QMT、cfquant Web 或通信服务重启后,待桥接策略恢复授权、通道可用,SDK 会自动恢复该账号的新回调;默认 Web 路由和直接 Pipe/LTtx 连接均适用。主动 `unsubscribe(account)` 后不会再恢复该账号,`stop()` / `disconnect()` 会停止后台恢复。此机制只恢复账号订阅,不补发停机期间的回调,也不重试委托或撤单。升级 SDK 后需先重启一次用户程序加载新代码,此后无需随 QMT 重启用户程序。
347
+
346
348
  推荐迁移顺序:**行情查询 → 资金/持仓 → 回调 → 模拟委托 → 模拟撤单 → 正式策略**。每一步都先在 Web 控制台接口测试和日志中确认,再进入下一步。
347
349
 
348
350
  ## Web 控制台
@@ -317,9 +317,11 @@ trader.disconnect()
317
317
 
318
318
  ### 回调和接口差异
319
319
 
320
- `XtQuantTrader`、`StockAccount`、常用行情查询和交易方法保持接近 `xtquant` 的调用方式,原有回调类通常可以继续使用。仍有部分 QMT 专有接口、字段和返回结构存在兼容边界,使用前请查看 [xtquant 原版接口适配清单](docs/xtquant原版接口适配清单.md) 和 [QMT 函数封装能力清单](docs/QMT函数封装能力清单.md)。
321
-
322
- 推荐迁移顺序:**行情查询 → 资金/持仓 → 回调 → 模拟委托 → 模拟撤单 → 正式策略**。每一步都先在 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
 
@@ -9,6 +9,11 @@ import sys
9
9
  import tempfile
10
10
  from pathlib import Path
11
11
 
12
+ if __package__:
13
+ from .qmt_dependencies import report_startup_dependencies
14
+ else:
15
+ from qmt_dependencies import report_startup_dependencies
16
+
12
17
 
13
18
  PACKAGE_NAME = "cfquant"
14
19
 
@@ -503,6 +508,7 @@ def ensure_cfquant_installed(project_root, python_exe=None):
503
508
  print("cfquant installed=%s project=%s" % (installed or "missing", expected))
504
509
  if installed == expected:
505
510
  print("Package versions match; skip installation.")
511
+ report_startup_dependencies(python_exe)
506
512
  return 0
507
513
 
508
514
  command = editable_install_args(project_root, python_exe)
@@ -526,6 +532,7 @@ def ensure_cfquant_installed(project_root, python_exe=None):
526
532
  print("[ERROR] Installed cfquant version does not match the project after installation.", file=sys.stderr)
527
533
  return 1
528
534
  print("%s package is installed." % PACKAGE_NAME)
535
+ report_startup_dependencies(python_exe)
529
536
  return 0
530
537
 
531
538
 
@@ -197,6 +197,8 @@ def batch_cancel_result_rows(cancels, seqs=None):
197
197
  def batch_result(account, batch_id, asynchronous, results, operation="order"):
198
198
  counts = {name: sum(row["status"] == name for row in results)
199
199
  for name in ("submitted", "failed", "unknown", "skipped")}
200
+ counts["rejected"] = sum(row.get("dispatched") is False for row in results)
201
+ counts["retryable"] = sum(row.get("safe_to_retry") is True for row in results)
200
202
  return dict(counts, batch_id=batch_id, account=account, asynchronous=asynchronous,
201
203
  operation=operation, execution="qmt", total=len(results), attempted=len(results) - counts["skipped"],
202
204
  ok=counts["submitted"] == len(results), results=results)
@@ -207,6 +209,19 @@ def batch_unknown(rows, error):
207
209
  row.update(status="unknown", ok=None, error=str(error) or type(error).__name__)
208
210
 
209
211
 
212
+ def batch_rejected(rows, error):
213
+ """Mark a rejection known to happen before QMT dispatch."""
214
+ details = getattr(error, "error_details", None)
215
+ details = details if isinstance(details, dict) else {}
216
+ message = str(error) or type(error).__name__
217
+ for row in rows:
218
+ row.update(status="failed", ok=False, error=message, dispatched=False,
219
+ safe_to_retry=details.get("safe_to_retry") is True,
220
+ retry_when=details.get("retry_when", ""),
221
+ error_code=details.get("code", ""),
222
+ error_stage=details.get("stage", "before_dispatch"))
223
+
224
+
210
225
  def validate_batch_response(result, params, asynchronous):
211
226
  expected = params["orders"]
212
227
  if (not isinstance(result, dict) or result.get("execution") != "qmt"
@@ -1,7 +1,7 @@
1
1
  """Order-only extensions sharing an existing XtQuantTrader session and callbacks."""
2
2
 
3
3
  from .batch_orders import (
4
- batch_cancel_result_rows, batch_result, batch_result_rows, batch_unknown,
4
+ batch_cancel_result_rows, batch_rejected, batch_result, batch_result_rows, batch_unknown,
5
5
  prepare_batch_cancels, prepare_batch_orders, validate_batch_cancel_response, validate_batch_response,
6
6
  )
7
7
  from .protocol import new_id
@@ -86,9 +86,18 @@ class CfQuantTrader:
86
86
  result = validate_batch_response(response, params, asynchronous)
87
87
  except Exception as error:
88
88
  results = batch_result_rows(rows, seqs)
89
- 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()
@@ -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):
@@ -18,9 +18,12 @@ class QmtNotReady(RuntimeError):
18
18
 
19
19
  def verify_environment(directory):
20
20
  """Validate dependencies and native ABI before stopping a running terminal."""
21
- import frida # noqa: F401
22
- import win32api
23
- import win32crypt # noqa: F401
21
+ try:
22
+ from .qmt_dependencies import LOGIN_MODULES, import_login_module
23
+ except ImportError:
24
+ from qmt_dependencies import LOGIN_MODULES, import_login_module
25
+ modules = {name: import_login_module(name) for name in LOGIN_MODULES}
26
+ win32api = modules["win32api"]
24
27
  directory = Path(directory)
25
28
  for module in ("Qt5Core.dll", "Qt5Widgets.dll"):
26
29
  version = win32api.GetFileVersionInfo(str(directory / module), "\\")
@@ -83,12 +86,16 @@ def select_binding(config_path, name):
83
86
 
84
87
  class QtLoginBackend:
85
88
  def __init__(self, directory, timeout=40, expected_pid=None):
86
- import frida
89
+ try:
90
+ from .qmt_dependencies import import_login_module
91
+ except ImportError:
92
+ from qmt_dependencies import import_login_module
87
93
  import psutil
88
- import win32api
89
- import win32event
90
- import win32gui
91
- import win32process
94
+ frida = import_login_module("frida")
95
+ win32api = import_login_module("win32api")
96
+ win32event = import_login_module("win32event")
97
+ win32gui = import_login_module("win32gui")
98
+ win32process = import_login_module("win32process")
92
99
  self.gui, self.process = win32gui, win32process
93
100
  self.timeout = timeout
94
101
  self.hwnd = None
@@ -232,8 +239,12 @@ class QtLoginBackend:
232
239
  self.failure_hwnd = None
233
240
 
234
241
  def close(self):
235
- import win32api
236
- import win32event
242
+ try:
243
+ from .qmt_dependencies import import_login_module
244
+ except ImportError:
245
+ from qmt_dependencies import import_login_module
246
+ win32api = import_login_module("win32api")
247
+ win32event = import_login_module("win32event")
237
248
  try:
238
249
  if self.session:
239
250
  self.session.detach()
@@ -6,6 +6,8 @@ import os
6
6
  from pathlib import Path
7
7
  import threading
8
8
 
9
+ from .qmt_dependencies import QmtDependencyError, import_login_module
10
+
9
11
  _LOCK = threading.RLock()
10
12
 
11
13
 
@@ -17,12 +19,12 @@ def directory_key(directory):
17
19
 
18
20
 
19
21
  def _protect(value, entropy):
20
- import win32crypt
22
+ win32crypt = import_login_module("win32crypt")
21
23
  return win32crypt.CryptProtectData(value, "CFQuant QMT login", entropy, None, None, 1)
22
24
 
23
25
 
24
26
  def _unprotect(value, entropy):
25
- import win32crypt
27
+ win32crypt = import_login_module("win32crypt")
26
28
  return win32crypt.CryptUnprotectData(value, entropy, None, None, 1)[1]
27
29
 
28
30
 
@@ -50,6 +52,8 @@ class QmtCredentialStore:
50
52
  key = directory_key(directory)
51
53
  try:
52
54
  encrypted = _protect(password.encode("utf-8"), key.encode("ascii"))
55
+ except QmtDependencyError:
56
+ raise
53
57
  except Exception:
54
58
  raise RuntimeError("无法使用当前 Windows 用户加密 QMT 密码,请检查 pywin32 安装") from None
55
59
  with _LOCK:
@@ -68,5 +72,7 @@ class QmtCredentialStore:
68
72
  return ""
69
73
  try:
70
74
  return _unprotect(base64.b64decode(entry["secret"]), key.encode("ascii")).decode("utf-8")
75
+ except QmtDependencyError:
76
+ raise
71
77
  except Exception:
72
78
  raise RuntimeError("无法解密 QMT 密码,请使用保存密码时的 Windows 用户运行,或重新保存密码") from None
@@ -0,0 +1,158 @@
1
+ """Read-only checks and repair instructions for optional QMT login components."""
2
+ import copy
3
+ import importlib
4
+ import json
5
+ import os
6
+ import subprocess
7
+ import sys
8
+ import threading
9
+ import time
10
+
11
+
12
+ LOGIN_MODULES = ("frida", "win32api", "win32crypt", "win32event", "win32gui", "win32process")
13
+ _LOCK = threading.Lock()
14
+ _CACHE = {}
15
+ _PROBE_CODE = r'''
16
+ import importlib, importlib.metadata, json, os, pathlib, sys
17
+ checks = []
18
+ for name in MODULES:
19
+ try:
20
+ module = importlib.import_module(name)
21
+ checks.append(dict(name=name, ok=True, path=getattr(module, '__file__', '')))
22
+ except Exception as error:
23
+ checks.append(dict(name=name, ok=False, error=str(error), error_type=type(error).__name__))
24
+ packages = {}
25
+ for name in ('pywin32', 'frida'):
26
+ try:
27
+ packages[name] = importlib.metadata.version(name)
28
+ except importlib.metadata.PackageNotFoundError:
29
+ packages[name] = ''
30
+ prefix = pathlib.Path(sys.prefix)
31
+ base_prefix = pathlib.Path(getattr(sys, 'base_prefix', sys.prefix))
32
+ conda = (prefix / 'conda-meta').is_dir()
33
+ venv = not conda and prefix != base_prefix
34
+ environment = 'conda' if conda else 'venv' if venv else 'python'
35
+ print(json.dumps(dict(python_executable=sys.executable, python_version=sys.version.split()[0],
36
+ prefix=str(prefix), base_prefix=str(base_prefix), environment=environment,
37
+ global_python=sys.prefix == sys.base_prefix,
38
+ conda_executable=os.environ.get('CONDA_EXE', '') if conda else '',
39
+ conda_pywin32=conda and bool(list((prefix / 'conda-meta').glob('pywin32-*.json'))),
40
+ supported=sys.platform == 'win32', checks=checks, packages=packages), ensure_ascii=True))
41
+ '''
42
+
43
+
44
+ def _ps_quote(value):
45
+ return "'" + str(value).replace("'", "''") + "'"
46
+
47
+
48
+ def _finish_report(report):
49
+ report["checked_at"] = time.time()
50
+ report["ok"] = bool(report.get("supported") and not report.get("error")
51
+ and all(item["ok"] for item in report.get("checks", [])))
52
+ failed = [item for item in report.get("checks", []) if not item["ok"]]
53
+ if not report.get("supported") and not report.get("error"):
54
+ report["message"] = "当前系统不支持 QMT 组件自动登录,请手动启动并登录 QMT。"
55
+ elif report["ok"]:
56
+ report["message"] = "QMT 自动登录组件检测通过。"
57
+ else:
58
+ names = "、".join(item["name"] for item in failed) or "Python 环境"
59
+ report["message"] = "QMT 自动登录组件不可用(%s)。可取消自动启动,保存配置后手动启动并登录 QMT。" % names
60
+ commands = []
61
+ python = "& " + _ps_quote(report["python_executable"])
62
+ environment = str(report.get("environment") or "").lower()
63
+ if environment not in {"conda", "venv", "python"}:
64
+ environment = "python" if report.get("global_python") else "venv"
65
+ if report.get("supported") and failed:
66
+ if any(item["name"].startswith("win32") for item in failed):
67
+ if environment == "conda" and report.get("conda_pywin32"):
68
+ conda = report.get("conda_executable") or "conda"
69
+ commands.append("& %s install --prefix %s --force-reinstall 'pywin32>=306'" % (
70
+ _ps_quote(conda), _ps_quote(report["prefix"])))
71
+ else:
72
+ commands.append(python + " -m pip install --no-cache-dir --upgrade --force-reinstall 'pywin32>=306'")
73
+ if environment == "python":
74
+ commands.append(python + " -m pywin32_postinstall -install")
75
+ if any(item["name"] == "frida" for item in failed):
76
+ commands.append(python + " -m pip install --no-cache-dir --upgrade --force-reinstall 'frida>=17.0,<18'")
77
+ commands.append(python + " -c " + _ps_quote("import " + ", ".join(LOGIN_MODULES) + "; print('QMT login components OK')"))
78
+ report["repair_commands"] = commands
79
+ if environment == "conda":
80
+ report["repair_note"] = (
81
+ "已检测到 Conda 环境。先停止 CFQuant,在已激活该环境的 Anaconda Prompt 或 PowerShell 中执行;"
82
+ "命令会绑定当前环境的前缀。验证通过后重启 CFQuant,再重新检测。"
83
+ )
84
+ elif environment == "venv":
85
+ report["repair_note"] = (
86
+ "已检测到 venv 虚拟环境。先停止 CFQuant,在普通 PowerShell 中执行下面的命令;"
87
+ "命令已绑定当前 venv 的 Python,不要切换到系统 Python,也不要运行 pywin32_postinstall。"
88
+ "验证通过后重启 CFQuant,再重新检测。"
89
+ )
90
+ else:
91
+ report["repair_note"] = (
92
+ "已检测到独立 Python 环境。先停止 CFQuant,在 PowerShell 中执行下面的命令;"
93
+ "命令已绑定当前 Python,安装完成后会执行 pywin32_postinstall。"
94
+ "验证通过后重启 CFQuant,再重新检测。"
95
+ )
96
+ return report
97
+
98
+
99
+ def check_login_dependencies(python_exe=None, refresh=False, timeout=15):
100
+ """Probe the actual interpreter without installing packages or loading DLLs here."""
101
+ executable = str(python_exe or sys.executable)
102
+ with _LOCK:
103
+ if not refresh and executable in _CACHE:
104
+ return copy.deepcopy(_CACHE[executable])
105
+ report = {"python_executable": executable, "supported": sys.platform == "win32", "checks": []}
106
+ if not report["supported"]:
107
+ _CACHE[executable] = _finish_report(report)
108
+ return copy.deepcopy(_CACHE[executable])
109
+ try:
110
+ run_kwargs = {}
111
+ if os.name == "nt":
112
+ run_kwargs["creationflags"] = getattr(subprocess, "CREATE_NO_WINDOW", 0)
113
+ completed = subprocess.run(
114
+ [executable, "-c", "MODULES = %r\n" % (LOGIN_MODULES,) + _PROBE_CODE],
115
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding="utf-8",
116
+ errors="replace", timeout=timeout,
117
+ **run_kwargs,
118
+ )
119
+ if completed.returncode:
120
+ raise RuntimeError((completed.stderr or completed.stdout or "Python dependency probe failed")[-4000:])
121
+ # Startup hooks may print before our JSON report.
122
+ report.update(json.loads(completed.stdout.strip().splitlines()[-1]))
123
+ except Exception as error:
124
+ report["error"] = str(error)
125
+ _CACHE[executable] = _finish_report(report)
126
+ return copy.deepcopy(_CACHE[executable])
127
+
128
+
129
+ class QmtDependencyError(RuntimeError):
130
+ pass
131
+
132
+
133
+ def import_login_module(name):
134
+ """Keep in-process import errors actionable, including stale loaded DLLs."""
135
+ try:
136
+ return importlib.import_module(name)
137
+ except (ImportError, OSError) as error:
138
+ raise QmtDependencyError(
139
+ "QMT 自动登录组件 %s 无法加载;运行环境:%s;原因:%s。"
140
+ "请查看自动登录组件检测中的修复说明,或取消自动启动后手动登录 QMT。"
141
+ % (name, sys.executable, error)
142
+ ) from error
143
+
144
+
145
+ def report_startup_dependencies(python_exe=None):
146
+ report = check_login_dependencies(python_exe, refresh=True)
147
+ if not report.get("supported"):
148
+ return report
149
+ level = "INFO" if report["ok"] else "WARN"
150
+ print("[%s] %s Python=%s" % (level, report["message"], report["python_executable"]))
151
+ for item in report["checks"]:
152
+ if not item["ok"]:
153
+ print("[WARN] %s: %s" % (item["name"], item["error"]))
154
+ if report.get("error"):
155
+ print("[WARN] %s" % report["error"])
156
+ for command in report["repair_commands"]:
157
+ print("[REPAIR] %s" % command)
158
+ return report
@@ -5,11 +5,23 @@ import os as _cq_os
5
5
  import sys as _cq_sys
6
6
  import threading as _cq_threading
7
7
  import time as _cq_time
8
+ import uuid as _cq_uuid
9
+
10
+
11
+ class ManagedStrategyError(RuntimeError):
12
+ """A rejection made before entering any QMT business dispatcher."""
13
+
14
+ def __init__(self, message, code, runtime_instance_id, retry_when=""):
15
+ super(ManagedStrategyError, self).__init__(message)
16
+ self.error_details = dict(code=code, stage="before_dispatch", dispatched=False,
17
+ safe_to_retry=bool(retry_when), retry_when=retry_when,
18
+ runtime_instance_id=runtime_instance_id)
8
19
 
9
20
 
10
21
  class ManagedPipeRuntime(object):
11
22
  def __init__(self, descriptor=None):
12
23
  self.descriptor = dict(descriptor or {})
24
+ self.runtime_instance_id = _cq_uuid.uuid4().hex
13
25
  self.active = False
14
26
  self.approved = False
15
27
  self.revoked = False
@@ -49,7 +61,8 @@ class ManagedPipeRuntime(object):
49
61
  state = "error" if self.failure else ("stopped" if not self.active or self.revoked
50
62
  else ("running" if ready and self.approved else "loaded"))
51
63
  return dict(self.descriptor, state=state, error=self.failure,
52
- pid=_cq_os.getpid(), updated_at=_cq_time.time())
64
+ pid=_cq_os.getpid(), updated_at=_cq_time.time(),
65
+ runtime_instance_id=self.runtime_instance_id)
53
66
 
54
67
  def control(self, desired):
55
68
  if desired.get("runtime_key") != self.descriptor.get("runtime_key"):
@@ -78,9 +91,18 @@ class ManagedPipeRuntime(object):
78
91
  return self.control(params or {})
79
92
  if action == "cfquant.status":
80
93
  return function(action, params, msg)
81
- if not self.approved:
82
- raise RuntimeError("Managed strategy is awaiting account mode confirmation from cfquant Web.")
83
- return dispatch_request(action, params, msg)
94
+ with self.mutex:
95
+ if not self.active or self.revoked:
96
+ raise ManagedStrategyError("The account's cfquant strategy mode has changed.",
97
+ "STRATEGY_MODE_CHANGED", self.runtime_instance_id)
98
+ if not self.approved:
99
+ raise ManagedStrategyError(
100
+ "Managed strategy is awaiting account mode confirmation from cfquant Web.",
101
+ "ACCOUNT_MODE_UNCONFIRMED", self.runtime_instance_id, "account_ready")
102
+ result = dispatch_request(action, params, msg)
103
+ if action == "cfquant.ping" and isinstance(result, dict):
104
+ result = dict(result, managed_runtime=self.status())
105
+ return result
84
106
  return guarded
85
107
 
86
108
  def wrap(self, function, reject=False):
@@ -89,7 +111,8 @@ class ManagedPipeRuntime(object):
89
111
  with self.mutex:
90
112
  if not self.active or self.revoked:
91
113
  if reject:
92
- raise RuntimeError("The account's cfquant strategy mode has changed.")
114
+ raise ManagedStrategyError("The account's cfquant strategy mode has changed.",
115
+ "STRATEGY_MODE_CHANGED", self.runtime_instance_id)
93
116
  return None
94
117
  self.calls += 1
95
118
  try: