cfquant 0.2.55__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 (210) hide show
  1. {cfquant-0.2.55 → cfquant-0.2.56}/PKG-INFO +3 -1
  2. {cfquant-0.2.55 → cfquant-0.2.56}/README.md +5 -3
  3. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/batch_orders.py +15 -0
  4. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/cftrader.py +21 -3
  5. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/client.py +13 -3
  6. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/normal_bridge.py +24 -0
  7. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/pipe_client.py +20 -13
  8. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/protocol.py +5 -4
  9. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/qmt_dependencies.py +33 -12
  10. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/qmt_pipe_runtime.py +28 -5
  11. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/qmt_strategy_deploy.py +66 -35
  12. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/rpc_timing.py +15 -0
  13. {cfquant-0.2.55 → 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
  14. 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
  15. cfquant-0.2.56/cfquant/tests/test_account_query_timing.py +131 -0
  16. cfquant-0.2.56/cfquant/tests/test_binding_live_refresh_browser.py +95 -0
  17. cfquant-0.2.56/cfquant/tests/test_callback_recovery.py +287 -0
  18. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_cftrader.py +17 -1
  19. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_cftrader_tutorial.py +5 -0
  20. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_market_timing.py +1 -1
  21. cfquant-0.2.56/cfquant/tests/test_qmt_dependencies.py +107 -0
  22. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_qmt_fileless.py +129 -3
  23. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_qmt_strategy_browser.py +14 -6
  24. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_qmt_strategy_deploy.py +29 -0
  25. cfquant-0.2.56/cfquant/tests/test_remote_version_check.py +72 -0
  26. cfquant-0.2.56/cfquant/tests/test_trader_subscription_recovery.py +345 -0
  27. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_tutorial_reader.py +3 -0
  28. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tx_trade_bridge.py +36 -12
  29. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/version.py +2 -2
  30. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/xttrader.py +201 -43
  31. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant.egg-info/PKG-INFO +3 -1
  32. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant.egg-info/SOURCES.txt +5 -0
  33. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant_web_server.py +463 -228
  34. {cfquant-0.2.55 → cfquant-0.2.56}/pyproject.toml +1 -1
  35. {cfquant-0.2.55 → cfquant-0.2.56}/qmt_scripts/CFQUANT_LITE.py +102 -15
  36. {cfquant-0.2.55 → 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
  37. {cfquant-0.2.55 → 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
  38. {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/app.js +199 -52
  39. {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/index.html +16 -5
  40. {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/styles.css +35 -6
  41. 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
  42. cfquant-0.2.55/cfquant/tests/test_qmt_dependencies.py +0 -55
  43. {cfquant-0.2.55 → cfquant-0.2.56}/LICENSE +0 -0
  44. {cfquant-0.2.55 → cfquant-0.2.56}/LTtx/__init__.py +0 -0
  45. {cfquant-0.2.55 → cfquant-0.2.56}/LTtx/tx/Config.txt +0 -0
  46. {cfquant-0.2.55 → cfquant-0.2.56}/LTtx/tx/LTtx_server.py +0 -0
  47. {cfquant-0.2.55 → cfquant-0.2.56}/LTtx/tx/__init__.py +0 -0
  48. {cfquant-0.2.55 → cfquant-0.2.56}/LTtx/tx/tx.py +0 -0
  49. {cfquant-0.2.55 → cfquant-0.2.56}/MANIFEST.in +0 -0
  50. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/__init__.py +0 -0
  51. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/_editable_install.py +0 -0
  52. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/account_routing.py +0 -0
  53. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/build_info.py +0 -0
  54. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/callback_dispatcher.py +0 -0
  55. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/channels.py +0 -0
  56. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/cli.py +0 -0
  57. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/config.py +0 -0
  58. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/diagnostics.py +0 -0
  59. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/flow_control.py +0 -0
  60. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/hub.py +0 -0
  61. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/level2.py +0 -0
  62. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/log_management.py +0 -0
  63. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/logging_i18n.py +0 -0
  64. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/management.py +0 -0
  65. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/order_identity.py +0 -0
  66. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/order_meta.py +0 -0
  67. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/pipe_bridge.py +0 -0
  68. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/pipe_hub.py +0 -0
  69. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/pipe_transport.py +0 -0
  70. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/qmt_binding_settings.py +0 -0
  71. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/qmt_bridge.py +0 -0
  72. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/qmt_component_agent.js +0 -0
  73. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/qmt_component_login.py +0 -0
  74. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/qmt_credentials.py +0 -0
  75. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/qmt_strategy_package.py +0 -0
  76. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/qmt_strategy_runtime.py +0 -0
  77. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/quote_history.py +0 -0
  78. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/runtime_report.py +0 -0
  79. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/stock_connect.py +0 -0
  80. {cfquant-0.2.55 → 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.55 → 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.55 → 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.55 → 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.55 → 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.55 → 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.55 → 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.55 → 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.55 → 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.55 → 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.55 → 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.55 → 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.55 → 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.55 → 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.55 → 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.55 → 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.55 → 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.55 → 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.55 → 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.55 → 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.55 → 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.55 → 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.55 → 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.55 → 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.55 → 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.55 → 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.55 → 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.55 → 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.55 → 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.55 → 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.55 → 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.55 → 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.55 → cfquant-0.2.56}/cfquant/tests/_helpers.py +0 -0
  113. {cfquant-0.2.55 → 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.55 → 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.55 → cfquant-0.2.56}/cfquant/tests/readme.md +0 -0
  116. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_advanced_parity.py +0 -0
  117. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_api_tester_browser.py +0 -0
  118. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_api_tester_web.py +0 -0
  119. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
  120. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_binding_runtime_browser.py +0 -0
  121. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_callback_requests.py +0 -0
  122. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_cftrader_web.py +0 -0
  123. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_credit_compat.py +0 -0
  124. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_dataframe_encoding.py +0 -0
  125. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_json_serialization.py +0 -0
  126. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_latency_flow_control.py +0 -0
  127. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_lite_account_routing.py +0 -0
  128. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_lite_deployment_paths.py +0 -0
  129. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_lite_parity.py +0 -0
  130. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_lite_qmt_dispatch.py +0 -0
  131. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_local_trading_calendar.py +0 -0
  132. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_log_management.py +0 -0
  133. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_management.py +0 -0
  134. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_management_batch.py +0 -0
  135. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_market_data_fallback.py +0 -0
  136. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_order_identity.py +0 -0
  137. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_order_time_callback.py +0 -0
  138. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_project_updater.py +0 -0
  139. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_python_api_reference.py +0 -0
  140. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_qmt_auto_login_browser.py +0 -0
  141. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_qmt_auto_login_integration.py +0 -0
  142. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_qmt_binding_settings.py +0 -0
  143. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_qmt_component_login.py +0 -0
  144. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_qmt_strategy_web.py +0 -0
  145. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_quote_latency.py +0 -0
  146. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_quote_latency_encoding.py +0 -0
  147. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
  148. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_startup_recovery.py +0 -0
  149. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_stock_connect.py +0 -0
  150. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_stock_connect_browser.py +0 -0
  151. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_table_conversion.py +0 -0
  152. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_trade_strategy_callback.py +0 -0
  153. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_trading_dates.py +0 -0
  154. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_tx_auto_import.py +0 -0
  155. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_tx_shutdown.py +0 -0
  156. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/trading_calendar.py +0 -0
  157. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tx.py +0 -0
  158. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/xtconstant.py +0 -0
  159. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/xtdata.py +0 -0
  160. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/xttype.py +0 -0
  161. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant.egg-info/dependency_links.txt +0 -0
  162. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant.egg-info/entry_points.txt +0 -0
  163. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant.egg-info/requires.txt +0 -0
  164. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant.egg-info/top_level.txt +0 -0
  165. {cfquant-0.2.55 → cfquant-0.2.56}/cfquant_pipe_hub.py +0 -0
  166. {cfquant-0.2.55 → cfquant-0.2.56}/qmt_scripts/CFQUANT.py +0 -0
  167. {cfquant-0.2.55 → cfquant-0.2.56}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +0 -0
  168. {cfquant-0.2.55 → cfquant-0.2.56}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
  169. {cfquant-0.2.55 → cfquant-0.2.56}/qmt_scripts/__init__.py +0 -0
  170. {cfquant-0.2.55 → 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
  171. {cfquant-0.2.55 → 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
  172. {cfquant-0.2.55 → 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
  173. {cfquant-0.2.55 → 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
  174. {cfquant-0.2.55 → 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
  175. {cfquant-0.2.55 → cfquant-0.2.56}/requirements.txt +0 -0
  176. {cfquant-0.2.55 → cfquant-0.2.56}/setup.cfg +0 -0
  177. {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/__init__.py +0 -0
  178. {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/api-tester.js +0 -0
  179. {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/avatars/amber-pulse.svg +0 -0
  180. {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/avatars/copper-grid.svg +0 -0
  181. {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/avatars/market-blue.svg +0 -0
  182. {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/avatars/rose-circuit.svg +0 -0
  183. {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/avatars/signal-green.svg +0 -0
  184. {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/avatars/slate-wave.svg +0 -0
  185. {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/avatars/teal-orbit.svg +0 -0
  186. {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/avatars/violet-node.svg +0 -0
  187. {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/cftrader-api-data.js +0 -0
  188. {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/docs.js +0 -0
  189. {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/favicon.svg +0 -0
  190. {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
  191. {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
  192. {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
  193. {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
  194. {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
  195. {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
  196. {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
  197. {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
  198. {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
  199. {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
  200. {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
  201. {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
  202. {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
  203. {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
  204. {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
  205. {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
  206. {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
  207. {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
  208. {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/python-api-data.js +0 -0
  209. {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/python-api.js +0 -0
  210. {cfquant-0.2.55 → 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.55
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
 
@@ -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):
@@ -13,7 +13,7 @@ LOGIN_MODULES = ("frida", "win32api", "win32crypt", "win32event", "win32gui", "w
13
13
  _LOCK = threading.Lock()
14
14
  _CACHE = {}
15
15
  _PROBE_CODE = r'''
16
- import importlib, importlib.metadata, json, pathlib, sys
16
+ import importlib, importlib.metadata, json, os, pathlib, sys
17
17
  checks = []
18
18
  for name in MODULES:
19
19
  try:
@@ -28,10 +28,14 @@ for name in ('pywin32', 'frida'):
28
28
  except importlib.metadata.PackageNotFoundError:
29
29
  packages[name] = ''
30
30
  prefix = pathlib.Path(sys.prefix)
31
+ base_prefix = pathlib.Path(getattr(sys, 'base_prefix', sys.prefix))
31
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'
32
35
  print(json.dumps(dict(python_executable=sys.executable, python_version=sys.version.split()[0],
33
- prefix=str(prefix), environment='conda' if conda else 'venv' if sys.prefix != sys.base_prefix else 'python',
36
+ prefix=str(prefix), base_prefix=str(base_prefix), environment=environment,
34
37
  global_python=sys.prefix == sys.base_prefix,
38
+ conda_executable=os.environ.get('CONDA_EXE', '') if conda else '',
35
39
  conda_pywin32=conda and bool(list((prefix / 'conda-meta').glob('pywin32-*.json'))),
36
40
  supported=sys.platform == 'win32', checks=checks, packages=packages), ensure_ascii=True))
37
41
  '''
@@ -55,23 +59,40 @@ def _finish_report(report):
55
59
  report["message"] = "QMT 自动登录组件不可用(%s)。可取消自动启动,保存配置后手动启动并登录 QMT。" % names
56
60
  commands = []
57
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"
58
65
  if report.get("supported") and failed:
59
66
  if any(item["name"].startswith("win32") for item in failed):
60
- if report.get("conda_pywin32"):
61
- commands.append("conda install --prefix %s --force-reinstall 'pywin32>=306'" % _ps_quote(report["prefix"]))
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"])))
62
71
  else:
63
- commands.append(python + " -m pip install --no-cache-dir --force-reinstall 'pywin32>=306'")
64
- if report.get("global_python"):
72
+ commands.append(python + " -m pip install --no-cache-dir --upgrade --force-reinstall 'pywin32>=306'")
73
+ if environment == "python":
65
74
  commands.append(python + " -m pywin32_postinstall -install")
66
75
  if any(item["name"] == "frida" for item in failed):
67
- commands.append(python + " -m pip install --no-cache-dir --force-reinstall 'frida>=17.0,<18'")
76
+ commands.append(python + " -m pip install --no-cache-dir --upgrade --force-reinstall 'frida>=17.0,<18'")
68
77
  commands.append(python + " -c " + _ps_quote("import " + ", ".join(LOGIN_MODULES) + "; print('QMT login components OK')"))
69
78
  report["repair_commands"] = commands
70
- report["repair_note"] = (
71
- "先停止 CFQuant,再在此电脑的 PowerShell 中执行;Conda 命令请使用 Anaconda PowerShell Prompt。"
72
- "验证通过后重启 CFQuant,再重新检测。若仍报 DLL 错误,需要检查旧 DLL 残留;"
73
- "虚拟环境中不要运行 pywin32_postinstall。"
74
- )
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
+ )
75
96
  return report
76
97
 
77
98
 
@@ -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:
@@ -5,7 +5,8 @@ import copy
5
5
  import hashlib
6
6
  import io
7
7
  import json
8
- import os
8
+ import logging
9
+ import os
9
10
  from pathlib import Path
10
11
  import re
11
12
  import threading
@@ -15,9 +16,12 @@ import traceback
15
16
  import uuid
16
17
  from xml.dom import minidom
17
18
 
18
- from .qmt_strategy_package import build_package
19
-
20
-
19
+ from .qmt_strategy_package import build_package
20
+
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
21
25
  SCRIPTS = {"ctypes": "CFQUANT_CTYPE_ALL_LOWLAT.py", "lite": "CFQUANT_LITE.py",
22
26
  "lttx": "CFQUANT.py"}
23
27
  MODE_ALIASES = {
@@ -577,9 +581,12 @@ class QmtStrategyManager:
577
581
  with self.lock:
578
582
  jobs = [job for job in self.jobs.values() if job["account_key"] == account_key]
579
583
  fields = ("root", "state", "message", "error", "mode", "updated_at", "backup")
580
- result = {"enabled": any(job.get("enabled") for job in jobs),
581
- "targets": [dict({k: job.get(k, "") for k in fields},
582
- strategies=[r["name"] for r in job.get("roles", [])]) for job in jobs]}
584
+ result = {"enabled": any(job.get("enabled") for job in jobs),
585
+ "targets": [dict({k: job.get(k, "") for k in fields},
586
+ strategies=[r["name"] for r in job.get("roles", [])],
587
+ control_sync=[dict(r.get("control_sync") or {}, role=r["role"],
588
+ strategy=r["name"])
589
+ for r in job.get("roles", [])]) for job in jobs]}
583
590
  if account_key in self.errors:
584
591
  detail = str(self.errors[account_key] or "策略部署失败")
585
592
  result.update(error=detail, message="账号已保存,策略部署失败: " + detail)
@@ -737,6 +744,8 @@ class QmtStrategyManager:
737
744
  def _sync_pipe_runtime(self, job):
738
745
  if self.runtime_request is None:
739
746
  return
747
+ from .channels import channels_for_bridge
748
+
740
749
  desired = self._desired(job)
741
750
  for role in job.get("retired_roles", []):
742
751
  try:
@@ -744,16 +753,34 @@ class QmtStrategyManager:
744
753
  except Exception:
745
754
  pass
746
755
  for role in job["roles"]:
756
+ identity = role["identity"]
757
+ sync = role.setdefault("control_sync", {})
758
+ previous_error = sync.get("last_error", "")
759
+ sync.update(last_attempt_at=time.time(), pipe_name=identity.get("pipe_name"),
760
+ request_channel=((identity.get("channels") or {}).get("normal")
761
+ or identity.get("normal_channel")
762
+ or channels_for_bridge(identity.get("bridge_id"))["normal"]))
747
763
  try:
748
- report = self.runtime_request(role["identity"], desired)
749
- if (isinstance(report, dict) and report.get("runtime_key") == desired["runtime_key"]
750
- and report.get("role") == role["role"]):
751
- # Only the external manager writes status files.
752
- _write_json(role["runtime_status_path"], report)
753
- except Exception:
764
+ report = self.runtime_request(identity, desired)
765
+ if (not isinstance(report, dict) or report.get("runtime_key") != desired["runtime_key"]
766
+ or report.get("role") != role["role"]):
767
+ raise ValueError("Managed strategy control response scope mismatch")
768
+ # Success records an acknowledged control, not necessarily a
769
+ # ready bridge: QMT may still be binding its trading context.
770
+ sync.update(last_success_at=time.time(), state=report.get("state"),
771
+ pid=report.get("pid"), generation=report.get("generation"), last_error="")
772
+ # Only the external manager writes status files.
773
+ _write_json(role["runtime_status_path"], report)
774
+ except Exception as error:
754
775
  # A stopped/not-yet-imported strategy has no Pipe endpoint.
755
- # Existing reports expire normally in _observe_runtime.
756
- pass
776
+ # Retry next cycle, but retain diagnostics instead of hiding
777
+ # why a restarted strategy is still awaiting confirmation.
778
+ detail = "%s: %s" % (type(error).__name__, str(error))
779
+ sync["last_error"] = detail
780
+ if detail != previous_error:
781
+ logger.warning("QMT strategy control sync failed account=%s role=%s pipe=%s channel=%s: %s",
782
+ job["account_key"], role["role"], sync["pipe_name"],
783
+ sync["request_channel"], detail)
757
784
 
758
785
  @staticmethod
759
786
  def _role_endpoint(role):
@@ -790,23 +817,18 @@ class QmtStrategyManager:
790
817
  with self.lock:
791
818
  changed = False
792
819
  for job in self.jobs.values():
793
- before = copy.deepcopy(job)
794
- if job.get("state") == "disabled":
795
- continue
796
- if job.get("enabled") and self._prepare_generation_transition(job):
797
- # A strategy can report an older generation after this
798
- # process starts. Publish the transition before QMT
799
- # launches the old container again.
800
- self._control(job)
801
- if job.get("state") == "error":
802
- if not job.get("control_generation") and not _generation_values(
803
- job.get("accepted_generations")):
804
- continue
805
- retry_count = int(job.get("deploy_retry_count") or 0)
806
- if retry_count >= 5 or time.time() < float(job.get("deploy_retry_after") or 0):
807
- continue
808
- try:
809
- self._advance(job)
820
+ before = copy.deepcopy(job)
821
+ if job.get("state") == "disabled":
822
+ continue
823
+ try:
824
+ transition = job.get("enabled") and self._prepare_generation_transition(job)
825
+ # A QMT restart loses its in-memory approval. Keep sending
826
+ # the selected mode even if offline deployment failed,
827
+ # its retries are exhausted, or its files are unavailable.
828
+ self._sync_pipe_runtime(job)
829
+ if transition:
830
+ self._control(job)
831
+ self._advance(job)
810
832
  except Exception as error:
811
833
  retry_count = int(job.get("deploy_retry_count") or 0) + 1
812
834
  job["deploy_retry_count"] = retry_count
@@ -826,12 +848,21 @@ class QmtStrategyManager:
826
848
  if changed:
827
849
  self._save()
828
850
 
829
- def _advance(self, job):
830
- root = qmt_root(job["root"], validate=True)
851
+ def _advance(self, job):
852
+ root = qmt_root(job["root"])
831
853
  if self.process_checker(root):
832
- self._sync_pipe_runtime(job)
833
854
  self._observe_runtime(job)
834
855
  return
856
+ # Retry limits apply to offline deployment, never to mode confirmation
857
+ # or observing a strategy that has come back online.
858
+ if job.get("state") == "error":
859
+ retry_count = int(job.get("deploy_retry_count") or 0)
860
+ if (not retry_count and not job.get("control_generation")
861
+ and not _generation_values(job.get("accepted_generations"))):
862
+ return
863
+ if retry_count >= 5 or time.time() < float(job.get("deploy_retry_after") or 0):
864
+ return
865
+ root = qmt_root(root, validate=True)
835
866
  job["retired_roles"] = []
836
867
  if (job.get("state") in ("configured", "running", "waiting_start", "waiting_manual_start")
837
868
  and all(role.get("imported") for role in job["roles"])):
@@ -7,23 +7,36 @@ class RpcTiming(object):
7
7
  self.output = output
8
8
  self.started = time.perf_counter()
9
9
  self.received_at = None
10
+ self.sent_at = None
10
11
  if output is not None:
11
12
  output.update({
12
13
  "request_id": request_id,
14
+ "started_at": time.time(),
13
15
  "qmt": {"available": False, "reason": "no_response"},
14
16
  "rpc": {"response_received": False},
15
17
  })
16
18
 
19
+ def sent(self):
20
+ self.sent_at = time.perf_counter()
21
+ if self.output is not None:
22
+ self.output["rpc"].update({
23
+ "sent_at": time.time(),
24
+ "connect_send_ms": round((self.sent_at - self.started) * 1000, 3),
25
+ })
26
+
17
27
  def received(self, message):
18
28
  self.received_at = time.perf_counter()
19
29
  if self.output is None:
20
30
  return
21
31
  self.output["rpc"].update({
32
+ "received_at": time.time(),
22
33
  "response_received": True,
23
34
  "response_ok": bool(message.get("ok")),
24
35
  "wait_ms": round((self.received_at - self.started) * 1000, 3),
25
36
  "json_decode_ms": message.get("_client_parse_ms"),
26
37
  })
38
+ if self.sent_at is not None:
39
+ self.output["rpc"]["response_wait_ms"] = round((self.received_at - self.sent_at) * 1000, 3)
27
40
  meta = message.get("meta") or {}
28
41
  qmt = (meta.get("timing") or {}).get("qmt")
29
42
  self.output["qmt"] = dict(qmt) if isinstance(qmt, dict) else {
@@ -44,3 +57,5 @@ class RpcTiming(object):
44
57
  self.output["rpc"]["total_ms"] = elapsed
45
58
  if self.received_at is None:
46
59
  self.output["rpc"]["wait_ms"] = elapsed
60
+ if self.sent_at is not None:
61
+ self.output["rpc"]["response_wait_ms"] = round((time.perf_counter() - self.sent_at) * 1000, 3)