cfquant 0.2.68__tar.gz → 0.2.70__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 (310) hide show
  1. {cfquant-0.2.68 → cfquant-0.2.70}/PKG-INFO +1 -1
  2. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/qmt_python_runtime.py +6 -1
  3. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/qmt_strategy_deploy.py +11 -3
  4. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_binding_python_runtime_browser.py +73 -0
  5. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_qmt_auto_login_integration.py +32 -0
  6. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_qmt_python_runtime.py +139 -0
  7. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_qmt_strategy_deploy.py +19 -0
  8. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/version.py +2 -2
  9. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant.egg-info/PKG-INFO +1 -1
  10. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant_web_server.py +163 -40
  11. {cfquant-0.2.68 → cfquant-0.2.70}/pyproject.toml +1 -1
  12. {cfquant-0.2.68 → cfquant-0.2.70}/qmt_scripts/CFQUANT_LITE.py +1 -1
  13. {cfquant-0.2.68 → cfquant-0.2.70}/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 +1 -1
  14. {cfquant-0.2.68 → cfquant-0.2.70}/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 +1 -1
  15. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/app.js +66 -44
  16. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/index.html +2 -2
  17. {cfquant-0.2.68 → cfquant-0.2.70}/LICENSE +0 -0
  18. {cfquant-0.2.68 → cfquant-0.2.70}/LTtx/__init__.py +0 -0
  19. {cfquant-0.2.68 → cfquant-0.2.70}/LTtx/tx/Config.txt +0 -0
  20. {cfquant-0.2.68 → cfquant-0.2.70}/LTtx/tx/LTtx_server.py +0 -0
  21. {cfquant-0.2.68 → cfquant-0.2.70}/LTtx/tx/__init__.py +0 -0
  22. {cfquant-0.2.68 → cfquant-0.2.70}/LTtx/tx/tx.py +0 -0
  23. {cfquant-0.2.68 → cfquant-0.2.70}/MANIFEST.in +0 -0
  24. {cfquant-0.2.68 → cfquant-0.2.70}/README.md +0 -0
  25. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/__init__.py +0 -0
  26. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/_editable_install.py +0 -0
  27. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/account_routing.py +0 -0
  28. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/algo.py +0 -0
  29. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/algo_broker.py +0 -0
  30. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/batch_orders.py +0 -0
  31. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/build_info.py +0 -0
  32. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/callback_dispatcher.py +0 -0
  33. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/cftrader.py +0 -0
  34. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/channels.py +0 -0
  35. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/cli.py +0 -0
  36. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/client.py +0 -0
  37. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/conditional_orders.py +0 -0
  38. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/config.py +0 -0
  39. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/diagnostic_context.py +0 -0
  40. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/diagnostics.py +0 -0
  41. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/flow_control.py +0 -0
  42. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/hub.py +0 -0
  43. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/level2.py +0 -0
  44. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/log_management.py +0 -0
  45. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/logging_i18n.py +0 -0
  46. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/management.py +0 -0
  47. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/monitor_notifications.py +0 -0
  48. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/monitor_performance.py +0 -0
  49. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/normal_bridge.py +0 -0
  50. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/notify.py +0 -0
  51. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/order_identity.py +0 -0
  52. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/order_meta.py +0 -0
  53. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/pipe_bridge.py +0 -0
  54. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/pipe_client.py +0 -0
  55. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/pipe_hub.py +0 -0
  56. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/pipe_transport.py +0 -0
  57. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/planning_broker.py +0 -0
  58. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/position_sizing.py +0 -0
  59. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/protocol.py +0 -0
  60. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/qmt_binding_defaults.py +0 -0
  61. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/qmt_binding_settings.py +0 -0
  62. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/qmt_bridge.py +0 -0
  63. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/qmt_component_agent.js +0 -0
  64. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/qmt_component_login.py +0 -0
  65. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/qmt_credentials.py +0 -0
  66. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/qmt_dependencies.py +0 -0
  67. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/qmt_inner.py +0 -0
  68. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/qmt_inner_catalog.py +0 -0
  69. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/qmt_inner_runtime.py +0 -0
  70. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/qmt_mac_agent.js +0 -0
  71. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/qmt_native_files.py +0 -0
  72. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/qmt_native_worker.py +0 -0
  73. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/qmt_pipe_runtime.py +0 -0
  74. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/qmt_strategy_agent.js +0 -0
  75. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/qmt_strategy_package.py +0 -0
  76. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/qmt_strategy_recovery.py +0 -0
  77. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/qmt_strategy_runtime.py +0 -0
  78. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/qmt_update.py +0 -0
  79. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/qmt_vm_agent.js +0 -0
  80. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/qmt_vm_discovery.py +0 -0
  81. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/qmt_vm_runtime.py +0 -0
  82. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/qmt_vm_settings.py +0 -0
  83. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/qmt_vm_worker.py +0 -0
  84. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/quote_history.py +0 -0
  85. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/quote_leases.py +0 -0
  86. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/quote_service.py +0 -0
  87. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/remote_adapter.py +0 -0
  88. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/remote_management.py +0 -0
  89. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/remote_supervisor.py +0 -0
  90. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/rpc_timing.py +0 -0
  91. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/runtime_report.py +0 -0
  92. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/shared_quotes.py +0 -0
  93. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/smart_orders.py +0 -0
  94. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/stock_connect.py +0 -0
  95. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/subscription_bridge.py +0 -0
  96. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/subscription_client.py +0 -0
  97. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/subscription_manager.py +0 -0
  98. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/subscription_store.py +0 -0
  99. {cfquant-0.2.68 → cfquant-0.2.70}/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
  100. {cfquant-0.2.68 → cfquant-0.2.70}/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
  101. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/12_/350/256/242/345/215/225/345/244/207/346/263/250/346/265/213/350/257/225.py" +0 -0
  102. {cfquant-0.2.68 → cfquant-0.2.70}/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
  103. {cfquant-0.2.68 → cfquant-0.2.70}/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
  104. {cfquant-0.2.68 → cfquant-0.2.70}/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
  105. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/16_/345/220/257/345/212/250/345/256/211/350/243/205/346/265/213/350/257/225.py" +0 -0
  106. {cfquant-0.2.68 → cfquant-0.2.70}/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
  107. {cfquant-0.2.68 → cfquant-0.2.70}/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
  108. {cfquant-0.2.68 → cfquant-0.2.70}/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
  109. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/1_/350/241/214/346/203/205/346/216/245/346/224/266/346/265/213/350/257/225.py" +0 -0
  110. {cfquant-0.2.68 → cfquant-0.2.70}/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
  111. {cfquant-0.2.68 → cfquant-0.2.70}/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
  112. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/22_/350/277/236/346/216/245/350/257/212/346/226/255/346/265/213/350/257/225.py" +0 -0
  113. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/23_/347/244/272/344/276/213/351/205/215/347/275/256/346/265/213/350/257/225.py" +0 -0
  114. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/24_/350/264/246/346/210/267/347/274/223/345/255/230/346/214/211/351/234/200/345/210/267/346/226/260/346/265/213/350/257/225.py" +0 -0
  115. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/25_/350/264/246/346/210/267/347/274/223/345/255/230/351/241/265/351/235/242/346/265/213/350/257/225.py" +0 -0
  116. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/26_/345/205/250/346/216/250/350/256/242/351/230/205/346/265/213/350/257/225.py" +0 -0
  117. {cfquant-0.2.68 → cfquant-0.2.70}/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
  118. {cfquant-0.2.68 → cfquant-0.2.70}/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
  119. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/29_Level2/350/241/214/346/203/205/346/265/213/350/257/225.py" +0 -0
  120. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/2_/346/225/260/346/215/256/350/216/267/345/217/226/346/265/213/350/257/225.py" +0 -0
  121. {cfquant-0.2.68 → cfquant-0.2.70}/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
  122. {cfquant-0.2.68 → cfquant-0.2.70}/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
  123. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/32_/345/274/202/346/255/245/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  124. {cfquant-0.2.68 → cfquant-0.2.70}/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
  125. {cfquant-0.2.68 → cfquant-0.2.70}/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
  126. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/3_/346/225/260/346/215/256/344/270/213/350/275/275/346/265/213/350/257/225.py" +0 -0
  127. {cfquant-0.2.68 → cfquant-0.2.70}/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
  128. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/5_/347/234/237/345/256/236/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  129. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/6_/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
  130. {cfquant-0.2.68 → cfquant-0.2.70}/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
  131. {cfquant-0.2.68 → cfquant-0.2.70}/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
  132. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/9_/345/217/257/346/222/244/350/256/242/345/215/225/346/265/213/350/257/225.py" +0 -0
  133. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/_helpers.py +0 -0
  134. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/qmt_vm_native_fixture.js +0 -0
  135. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/readme.md +0 -0
  136. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_account_query_timing.py +0 -0
  137. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_account_subscription_probe.py +0 -0
  138. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_activity_log_browser.py +0 -0
  139. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_advanced_parity.py +0 -0
  140. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_algo.py +0 -0
  141. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_algo_broker.py +0 -0
  142. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_api_tester_browser.py +0 -0
  143. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_api_tester_web.py +0 -0
  144. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_auto_login_app.py +0 -0
  145. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
  146. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_binding_live_refresh_browser.py +0 -0
  147. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_binding_runtime_browser.py +0 -0
  148. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_binding_vm_browser.py +0 -0
  149. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_binding_webhook_browser.py +0 -0
  150. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_callback_recovery.py +0 -0
  151. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_callback_requests.py +0 -0
  152. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_cftrader.py +0 -0
  153. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_cftrader_tutorial.py +0 -0
  154. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_cftrader_web.py +0 -0
  155. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_credit_compat.py +0 -0
  156. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_dataframe_encoding.py +0 -0
  157. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_diagnostic_context.py +0 -0
  158. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_history_download_memory.py +0 -0
  159. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_json_serialization.py +0 -0
  160. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_latency_flow_control.py +0 -0
  161. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_lite_account_routing.py +0 -0
  162. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_lite_deployment_paths.py +0 -0
  163. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_lite_parity.py +0 -0
  164. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_lite_qmt_dispatch.py +0 -0
  165. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_local_trading_calendar.py +0 -0
  166. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_log_management.py +0 -0
  167. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_management.py +0 -0
  168. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_management_batch.py +0 -0
  169. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_market_data_fallback.py +0 -0
  170. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_market_timing.py +0 -0
  171. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_monitor_notifications.py +0 -0
  172. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_monitor_performance.py +0 -0
  173. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_navigation_browser.py +0 -0
  174. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_notify.py +0 -0
  175. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_order_identity.py +0 -0
  176. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_order_time_callback.py +0 -0
  177. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_pipe_shutdown.py +0 -0
  178. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_project_updater.py +0 -0
  179. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_python_api_reference.py +0 -0
  180. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_qmt_auto_login_browser.py +0 -0
  181. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_qmt_binding_defaults.py +0 -0
  182. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_qmt_binding_settings.py +0 -0
  183. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_qmt_component_login.py +0 -0
  184. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_qmt_dependencies.py +0 -0
  185. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_qmt_fileless.py +0 -0
  186. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_qmt_inner.py +0 -0
  187. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_qmt_inner_reference.py +0 -0
  188. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_qmt_inner_web.py +0 -0
  189. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_qmt_native_files.py +0 -0
  190. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_qmt_native_reuse.py +0 -0
  191. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_qmt_native_worker.py +0 -0
  192. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_qmt_package_import.py +0 -0
  193. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_qmt_strategy_browser.py +0 -0
  194. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_qmt_strategy_capabilities.py +0 -0
  195. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_qmt_strategy_recovery.py +0 -0
  196. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_qmt_strategy_web.py +0 -0
  197. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_qmt_update.py +0 -0
  198. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_qmt_vm_discovery.py +0 -0
  199. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_qmt_vm_native.py +0 -0
  200. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_qmt_vm_runtime.py +0 -0
  201. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_qmt_vm_settings.py +0 -0
  202. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_quote_latency.py +0 -0
  203. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_quote_latency_encoding.py +0 -0
  204. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_quote_service.py +0 -0
  205. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_remote_management.py +0 -0
  206. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_remote_management_browser.py +0 -0
  207. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_remote_version_check.py +0 -0
  208. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
  209. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_shared_quotes.py +0 -0
  210. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_shared_quotes_generated.py +0 -0
  211. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_smart_orders.py +0 -0
  212. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_smart_orders_api.py +0 -0
  213. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_smart_orders_docs.py +0 -0
  214. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_split_order_identity.py +0 -0
  215. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_startup_recovery.py +0 -0
  216. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_stock_connect.py +0 -0
  217. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_stock_connect_browser.py +0 -0
  218. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_strategy_recovery_api.py +0 -0
  219. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_subscription_congestion.py +0 -0
  220. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_subscription_persistence.py +0 -0
  221. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_table_conversion.py +0 -0
  222. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_trade_strategy_callback.py +0 -0
  223. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_trader_subscription_recovery.py +0 -0
  224. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_trading_dates.py +0 -0
  225. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_trading_monitor.py +0 -0
  226. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_trading_monitor_browser.py +0 -0
  227. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_tutorial_reader.py +0 -0
  228. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_tx_auto_import.py +0 -0
  229. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_tx_shutdown.py +0 -0
  230. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_version_check.py +0 -0
  231. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tests/test_version_check_browser.py +0 -0
  232. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tools/THIRD_PARTY.md +0 -0
  233. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tools/upx/COPYING +0 -0
  234. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tools/upx/LICENSE +0 -0
  235. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tools/upx/NEWS +0 -0
  236. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tools/upx/README +0 -0
  237. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tools/upx/THANKS.txt +0 -0
  238. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tools/upx/upx-doc.html +0 -0
  239. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tools/upx/upx-doc.txt +0 -0
  240. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tools/upx/upx.1 +0 -0
  241. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tools/upx/upx.exe +0 -0
  242. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/trading_calendar.py +0 -0
  243. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/trading_monitor.py +0 -0
  244. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tx.py +0 -0
  245. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/tx_trade_bridge.py +0 -0
  246. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/webhook.py +0 -0
  247. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/xtconstant.py +0 -0
  248. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/xtdata.py +0 -0
  249. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/xttrader.py +0 -0
  250. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant/xttype.py +0 -0
  251. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant.egg-info/SOURCES.txt +0 -0
  252. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant.egg-info/dependency_links.txt +0 -0
  253. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant.egg-info/entry_points.txt +0 -0
  254. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant.egg-info/requires.txt +0 -0
  255. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant.egg-info/top_level.txt +0 -0
  256. {cfquant-0.2.68 → cfquant-0.2.70}/cfquant_pipe_hub.py +0 -0
  257. {cfquant-0.2.68 → cfquant-0.2.70}/qmt_scripts/CFQUANT.py +0 -0
  258. {cfquant-0.2.68 → cfquant-0.2.70}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +0 -0
  259. {cfquant-0.2.68 → cfquant-0.2.70}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
  260. {cfquant-0.2.68 → cfquant-0.2.70}/qmt_scripts/__init__.py +0 -0
  261. {cfquant-0.2.68 → cfquant-0.2.70}/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
  262. {cfquant-0.2.68 → cfquant-0.2.70}/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
  263. {cfquant-0.2.68 → cfquant-0.2.70}/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
  264. {cfquant-0.2.68 → cfquant-0.2.70}/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
  265. {cfquant-0.2.68 → cfquant-0.2.70}/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
  266. {cfquant-0.2.68 → cfquant-0.2.70}/requirements.txt +0 -0
  267. {cfquant-0.2.68 → cfquant-0.2.70}/setup.cfg +0 -0
  268. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/__init__.py +0 -0
  269. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/activity-log.js +0 -0
  270. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/api-tester.js +0 -0
  271. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/avatars/amber-pulse.svg +0 -0
  272. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/avatars/copper-grid.svg +0 -0
  273. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/avatars/market-blue.svg +0 -0
  274. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/avatars/rose-circuit.svg +0 -0
  275. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/avatars/signal-green.svg +0 -0
  276. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/avatars/slate-wave.svg +0 -0
  277. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/avatars/teal-orbit.svg +0 -0
  278. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/avatars/violet-node.svg +0 -0
  279. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/cfquant-architecture.html +0 -0
  280. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/cftrader-api-data.js +0 -0
  281. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/docs.js +0 -0
  282. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/favicon.svg +0 -0
  283. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
  284. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
  285. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
  286. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
  287. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
  288. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
  289. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
  290. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
  291. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
  292. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
  293. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
  294. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
  295. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
  296. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
  297. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
  298. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
  299. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
  300. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
  301. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/navigation.js +0 -0
  302. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/notify-api-data.js +0 -0
  303. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/python-api-data.js +0 -0
  304. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/python-api.js +0 -0
  305. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/qmt-inner-api-data.js +0 -0
  306. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/remote-management.js +0 -0
  307. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/smart-order-api-data.js +0 -0
  308. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/styles.css +0 -0
  309. {cfquant-0.2.68 → cfquant-0.2.70}/web_dashboard/trading-monitor.js +0 -0
  310. {cfquant-0.2.68 → cfquant-0.2.70}/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.68
3
+ Version: 0.2.70
4
4
  Summary: QMT local bridge and xtquant-compatible Python client.
5
5
  License: MIT
6
6
  Requires-Python: >=3.8
@@ -655,7 +655,12 @@ class QmtPythonRuntime:
655
655
  if (account_id or self.account_id) else None
656
656
  )
657
657
  finally:
658
- if restart and (not deferred_account_config or was_running):
658
+ # A managed Web binding can supply a bootstrap callback that starts
659
+ # QMT, waits for the first login to create Config.xml, and then
660
+ # performs the final runtime switch/restart. Keep the standalone
661
+ # API's old safe behavior (leave a brand-new, unconfigured QMT
662
+ # stopped) when no callback was supplied.
663
+ if restart and (not deferred_account_config or was_running or restart_callback):
659
664
  _callback(progress_callback, QmtPythonProgress(runtime, "restarting", message="正在重新启动 QMT"))
660
665
  if restart_callback:
661
666
  restarted = restart_callback()
@@ -82,7 +82,7 @@ STATES = {
82
82
  "waiting_start": "QMT 已启动,等待托管策略启动及通道连接",
83
83
  "waiting_manual_start": "QMT 已启动,自启未开启,请在模型交易中运行托管策略",
84
84
  "running": "托管策略已运行",
85
- "waiting_account": "未找到此资金账号的 QMT 绑定键,请补充模型账号 Key",
85
+ "waiting_account": "等待 QMT 登录生成账号绑定键;首次登录后请正常退出 QMT,再重新启动以完成策略部署。仍无法识别时再补充模型账号 Key",
86
86
  "configured": "模型已配置,等待下次启动 QMT 生效",
87
87
  "disabled": "自动运行已关闭,托管策略已禁用",
88
88
  "error": "策略部署失败",
@@ -915,15 +915,16 @@ class QmtStrategyManager:
915
915
  self._save()
916
916
 
917
917
  def prepare_for_start(self, directory):
918
- """Finish this terminal's offline deployment before allowing launch."""
918
+ """Finish offline deployment, or allow login to supply a missing key."""
919
919
  root = qmt_root(directory)
920
+ waiting_account = []
920
921
  with self.lock:
921
922
  if self.load_error:
922
923
  raise ValueError(self.load_error)
923
924
  jobs = [job for job in self.jobs.values()
924
925
  if os.path.normcase(str(qmt_root(job["root"]))) == os.path.normcase(str(root))]
925
926
  if not jobs:
926
- return
927
+ return {"waiting_account": waiting_account}
927
928
  if self.process_checker(root):
928
929
  raise RuntimeError("QMT 尚未完全退出,无法完成启动前策略部署")
929
930
  try:
@@ -957,6 +958,12 @@ class QmtStrategyManager:
957
958
  job["deploy_retry_after"] = time.time() + min(10.0, 2.0 * count)
958
959
  self._state(job, "error", str(error))
959
960
  raise
961
+ if job.get("state") == "waiting_account":
962
+ # QMT creates its account authorization on first login.
963
+ # Do not fabricate a model key or make login depend on
964
+ # the model that can only be prepared after that login.
965
+ waiting_account.append(job["account_id"])
966
+ continue
960
967
  if (job.get("state") not in ("configured", "waiting_import", "disabled")
961
968
  or (job.get("enabled") and any(
962
969
  not role.get("model_prepared") or role.get("reimport_required")
@@ -965,6 +972,7 @@ class QmtStrategyManager:
965
972
  (job.get("message") or job.get("state", "")))
966
973
  finally:
967
974
  self._save()
975
+ return {"waiting_account": waiting_account}
968
976
 
969
977
  def _advance(self, job):
970
978
  root = qmt_root(job["root"])
@@ -81,6 +81,15 @@ def test_selected_executor_is_applied_through_existing_job_until_restart_finishe
81
81
  expect(button).to_be_disabled()
82
82
  assert not page.locator("#bindingPythonRuntimeProgressBar").evaluate("node => node.classList.contains('is-done')")
83
83
  expect(page.locator("#bindingPythonRuntimeProgressMeta")).to_contain_text("等待切换")
84
+ for phase in ("extract", "activating", "restarting"):
85
+ page.evaluate("""phase => bindingPythonRuntimeProgressEvent({
86
+ meta: {job_id: state.bindingPythonRuntimeJobId, stage: phase},
87
+ data: {phase, percent: 0, eta: null}
88
+ })""", phase)
89
+ expect(page.locator("#bindingPythonRuntimeProgressMeta")).not_to_contain_text("0%")
90
+ expect(page.locator("#bindingPythonRuntimeProgressMeta")).not_to_contain_text("剩余约 0")
91
+ assert page.locator("#bindingPythonRuntimeProgressBar").evaluate("node => node.getBoundingClientRect().width > 0")
92
+ assert page.evaluate("!!state.bindingPythonRuntimeJobId")
84
93
  status["binding"].update(selected_runtime="py312", configured_runtime="py312", state="ready")
85
94
  status["binding"]["runtimes"]["py312"]["installed"] = True
86
95
  page.evaluate("""bindingPythonRuntimeProgressEvent({
@@ -95,6 +104,45 @@ def test_selected_executor_is_applied_through_existing_job_until_restart_finishe
95
104
  assert not errors
96
105
 
97
106
 
107
+ def test_lost_job_record_releases_busy_state_without_claiming_success(page, frontend_url):
108
+ errors = open_runtime(page, frontend_url, runtime_status(config_exists=False))
109
+ page.route("**/api/qmt/python-runtime/download", lambda route: route.fulfill(json={"ok": True, "data": {"state": "queued"}}))
110
+ page.route("**/api/qmt/python-runtime/jobs?*", lambda route: route.fulfill(status=404, json={"ok": False, "error": "job not found"}))
111
+ page.locator("#bindingPythonRuntimeDownloadBtn").click()
112
+ page.wait_for_function("!state.bindingPythonRuntimeJobId && state.bindingPythonRuntimeProgress?.meta?.stage === 'error'")
113
+ expect(page.locator("#bindingPythonRuntimeProgressText")).to_contain_text("记录已失效")
114
+ expect(page.locator("#bindingPythonRuntimeDownloadBtn")).to_be_enabled()
115
+ assert not errors
116
+
117
+
118
+ def test_save_rechecks_completed_runtime_job_even_without_progress_notification(page, frontend_url):
119
+ errors = open_runtime(page, frontend_url, runtime_status(config_exists=False))
120
+ page.route("**/api/qmt/python-runtime/jobs?*", lambda route: route.fulfill(json={
121
+ "ok": True, "data": {"state": "done", "result": {"account_config_required": True}}}))
122
+ saved = []
123
+ def save(route):
124
+ body = route.request.post_data_json
125
+ saved.append(body)
126
+ row = dict(body, account_key="TEST_ONLY|STOCK|test", bridge_id="test")
127
+ route.fulfill(json={"ok": True, "data": {
128
+ "account": row, "account_configs": {row["account_key"]: row}, "account_pairs": {},
129
+ "bridges": {"test": {"name": "Test"}}, "qmt_strategy_deploy": {"enabled": False},
130
+ "qmt_auto_login": {"enabled": False}}})
131
+ page.route("**/api/account-config", save)
132
+ page.locator("#bindingQmtAutoLogin").uncheck()
133
+ page.evaluate("""() => {
134
+ fillQmtStrategySettings('binding', {enabled: false});
135
+ state.bindingPythonRuntimeJobId = 'missed-done';
136
+ state.bindingPythonRuntimeJobContext = bindingPythonRuntimeContextKey();
137
+ state.bindingPythonRuntimeJobRuntime = 'py36';
138
+ }""")
139
+ page.locator('#bindingForm [type="submit"]').click()
140
+ expect(page.locator("#bindingDialogOverlay")).not_to_be_visible()
141
+ assert len(saved) == 1
142
+ assert not page.evaluate("state.bindingPythonRuntimeJobId")
143
+ assert not errors
144
+
145
+
98
146
  def test_directory_change_invalidates_options_and_discards_late_response(page, frontend_url):
99
147
  errors = open_runtime(page, frontend_url, runtime_status())
100
148
  pending = []
@@ -129,3 +177,28 @@ def test_missing_account_config_shows_capabilities_but_requires_login(page, fron
129
177
  page.wait_for_function("state.bindingPythonRuntimeJobId")
130
178
  assert submitted[0]["allow_missing_account_config"] is True
131
179
  assert not errors
180
+
181
+
182
+ @pytest.mark.parametrize("socket_throws", [False, True], ids=["socket-disconnected", "socket-unavailable"])
183
+ def test_poll_completion_before_first_login_unlocks_binding(page, frontend_url, socket_throws):
184
+ status = runtime_status(config_exists=False)
185
+ errors = open_runtime(page, frontend_url, status)
186
+ submitted = []
187
+ page.route("**/api/qmt/python-runtime/download", lambda route: (
188
+ submitted.append(route.request.post_data_json),
189
+ route.fulfill(json={"ok": True, "data": {"state": "queued"}})))
190
+ # The last progress event is not a terminal event. HTTP state/result must
191
+ # recover completion even if the WebSocket notification never arrived.
192
+ page.route("**/api/qmt/python-runtime/jobs?*", lambda route: route.fulfill(json={
193
+ "ok": True, "data": {"state": "done", "progress": {"phase": "installed", "percent": 100},
194
+ "result": {"account_config_required": True, "restarted": None}}}))
195
+ if socket_throws:
196
+ page.evaluate("() => { window.WebSocket = function() { throw new Error('WebSocket unavailable'); }; }")
197
+ page.locator("#bindingPythonRuntimeDownloadBtn").click()
198
+ page.wait_for_function("state.bindingPythonRuntimeJobId === '' && state.bindingPythonRuntimeProgress?.meta?.stage === 'done'")
199
+ assert len(submitted) == 1
200
+ assert page.evaluate("state.bindingPythonRuntimeDeferred")
201
+ assert not page.evaluate("state.bindingPythonRuntimeChoiceDirty")
202
+ expect(page.locator("#bindingPythonRuntimeProgressMeta")).to_contain_text("100%")
203
+ expect(page.locator("#bindingPythonRuntimeProgressText")).to_contain_text("登录")
204
+ assert not errors
@@ -8,6 +8,7 @@ from types import SimpleNamespace
8
8
  import pytest
9
9
 
10
10
  from cfquant.tests.test_qmt_strategy_web import web_config, login_credentials
11
+ from cfquant.tests.test_qmt_strategy_deploy import deployment
11
12
  from cfquant.tests.test_management import service
12
13
  from cfquant.qmt_credentials import QmtCredentialStore
13
14
  from cfquant.trading_calendar import is_trading_day
@@ -286,6 +287,37 @@ def test_failed_offline_deployment_blocks_launch_and_login(web_config, monkeypat
286
287
  assert not result["started"] and not result["configured"]
287
288
 
288
289
 
290
+ @pytest.mark.parametrize("auto_fill", [True, False])
291
+ @pytest.mark.parametrize("restart", [True, False])
292
+ def test_first_login_without_model_key_launches_and_reports_pending_deployment(
293
+ web_config, deployment, monkeypatch, tmp_path, auto_fill, restart):
294
+ web, _ = web_config
295
+ manager, row, infos, running, root = deployment
296
+ (root / "config" / "indexUserConfig.xml").write_text(
297
+ '<ICUserConfigFile><FormulaCatalog/><strategyTrade/></ICUserConfigFile>', encoding="utf-8")
298
+ manager.configure(row, infos)
299
+ row["qmt_auto_login"] = {"enabled": True, "auto_fill_credentials": auto_fill, "auto_update": True}
300
+ monkeypatch.setenv("CFQUANT_QMT_AUTO_START", "saved")
301
+ monkeypatch.setattr(web, "QMT_AUTO_LOGIN_STATE_FILE", str(tmp_path / "sessions.json"))
302
+ monkeypatch.setattr(web, "QMT_COMPONENT_JOBS", {})
303
+ monkeypatch.setattr(web, "QMT_STRATEGIES", manager)
304
+ paths = {"bin_dir": str(root / "bin.x64"), "exe_path": "fixture.exe"}
305
+ monkeypatch.setattr(web, "_qmt_auto_login_paths", lambda _: paths)
306
+ monkeypatch.setattr(web, "_qmt_component_login_preflight", lambda *a: None)
307
+ monkeypatch.setattr(web, "_qmt_auto_login_processes", lambda *a, **k: [])
308
+ monkeypatch.setattr(web, "_qmt_auto_login_stop_xtitclient", lambda *a, **k: {})
309
+ calls = []
310
+ monkeypatch.setattr(web, "_qmt_auto_login_start_xtitclient", lambda *a, **k: calls.append("start") or {"pid": 456})
311
+ monkeypatch.setattr(web, "_qmt_queue_component_login", lambda *a: calls.append("login"))
312
+ result = web.qmt_auto_login_apply_for_account(row, restart=restart)
313
+ assert result["configured"] and not result.get("error")
314
+ assert result["strategy_deploy_deferred"]
315
+ assert calls == ["start", "login"]
316
+ session = web._qmt_auto_login_saved_session(row)
317
+ assert session["strategy_deploy_deferred"] and not session["error"]
318
+ assert "首次登录" in session["message"]
319
+
320
+
289
321
  def test_worker_records_success_and_supplies_saved_password_only_on_request(web_config, monkeypatch, tmp_path):
290
322
  import cfquant.qmt_component_login as component
291
323
  web, config = web_config
@@ -250,6 +250,36 @@ def test_missing_account_config_can_install_runtime_before_first_login(tmp_path,
250
250
  assert not (root / "userdata" / "users" / "55009948" / "Config.xml").exists()
251
251
 
252
252
 
253
+ def test_missing_account_config_invokes_managed_restart_callback(tmp_path, monkeypatch):
254
+ root = tmp_path / "qmt"
255
+ (root / "bin.x64").mkdir(parents=True)
256
+ manager = QmtPythonRuntime(root, account_id="55009948")
257
+ archive = manager.download_dir / "Python36-managed-bootstrap.zip"
258
+ archive.parent.mkdir(parents=True)
259
+ with zipfile.ZipFile(archive, "w") as bundle:
260
+ bundle.writestr("python36.dll", "new-dll")
261
+ bundle.writestr("Lib/site.py", "# test")
262
+ package = _package("py36", archive)
263
+ monkeypatch.setattr(manager, "fetch_package", lambda runtime: package)
264
+ monkeypatch.setattr(manager, "download", lambda *args, **kwargs: archive)
265
+ monkeypatch.setattr(manager, "_qmt_processes", lambda: [])
266
+ calls = []
267
+
268
+ def restart_callback():
269
+ calls.append("restart")
270
+ return {"started": True, "bootstrap_completed": True}
271
+
272
+ result = manager.download_and_switch(
273
+ "py36", account_id="55009948", restart=True,
274
+ allow_missing_account_config=True, restart_callback=restart_callback,
275
+ )
276
+
277
+ assert calls == ["restart"]
278
+ assert result["account_config_required"] is True
279
+ assert result["restarted"]["bootstrap_completed"] is True
280
+ assert not (root / "userdata" / "users" / "55009948" / "Config.xml").exists()
281
+
282
+
253
283
  def test_runtime_status_without_account_still_exposes_capabilities(tmp_path, monkeypatch):
254
284
  import cfquant_web_server as web
255
285
  manager = runtime_fixture(tmp_path)
@@ -307,3 +337,112 @@ def test_runtime_job_rejects_unsupported_selection_before_scheduling(tmp_path, m
307
337
  with pytest.raises(QmtPythonRuntimeError, match="only supports"):
308
338
  web.start_qmt_python_runtime_job({"qmt_dir": str(manager.qmt_dir), "account_id": "TEST_ONLY", "runtime": "py312"})
309
339
  assert not jobs
340
+
341
+
342
+ def test_runtime_job_completion_survives_notification_failure(tmp_path, monkeypatch):
343
+ import cfquant_web_server as web
344
+ manager = runtime_fixture(tmp_path)
345
+ jobs = {}
346
+ monkeypatch.setattr(web, "QMT_PYTHON_RUNTIME_JOBS", jobs)
347
+ monkeypatch.setattr(web.threading.Thread, "start", lambda self: None)
348
+ def unavailable(*args, **kwargs):
349
+ raise RuntimeError("callback transport unavailable")
350
+ monkeypatch.setattr(web, "emit_download_progress", unavailable)
351
+ result = {"account_config_required": True, "installed": {"installed": True}, "restarted": None}
352
+ def switch(self, runtime, **kwargs):
353
+ kwargs["progress_callback"]({"phase": "download", "percent": 50})
354
+ kwargs["progress_callback"]({"phase": "installed", "percent": 100})
355
+ return result
356
+ monkeypatch.setattr(QmtPythonRuntime, "download_and_switch", switch)
357
+ body = {"qmt_dir": str(manager.qmt_dir), "account_id": "TEST_ONLY", "runtime": "py36", "job_id": "notify-failure"}
358
+ web.start_qmt_python_runtime_job(body)
359
+ web._run_qmt_python_runtime_job(body["job_id"], body)
360
+ job = web._qmt_python_job_snapshot(body["job_id"])
361
+ assert job["state"] == "done"
362
+ assert job["result"] == result
363
+ assert job["progress"]["phase"] == "done"
364
+ assert job["progress"]["percent"] == 100
365
+
366
+
367
+ def test_runtime_job_initialization_failure_is_terminal(tmp_path, monkeypatch):
368
+ import cfquant_web_server as web
369
+ manager = runtime_fixture(tmp_path)
370
+ monkeypatch.setattr(web, "QMT_PYTHON_RUNTIME_JOBS", {})
371
+ monkeypatch.setattr(web.threading.Thread, "start", lambda self: None)
372
+ monkeypatch.setattr(web, "emit_download_progress", lambda *a, **kw: None)
373
+ body = {"qmt_dir": str(manager.qmt_dir), "runtime": "py36", "job_id": "init-failure"}
374
+ web.start_qmt_python_runtime_job(body)
375
+ def invalid(_):
376
+ raise ValueError("QMT directory unavailable")
377
+ monkeypatch.setattr(web, "_qmt_python_runtime_row", invalid)
378
+ web._run_qmt_python_runtime_job(body["job_id"], body)
379
+ assert web._qmt_python_job_snapshot(body["job_id"])["state"] == "error"
380
+
381
+
382
+ @pytest.mark.parametrize("restart_failed", [False, True])
383
+ def test_runtime_job_uses_managed_restart_without_inline_login_or_bypassing_errors(tmp_path, monkeypatch, restart_failed):
384
+ from types import SimpleNamespace
385
+ import cfquant_web_server as web
386
+ manager = runtime_fixture(tmp_path)
387
+ row = {"account_id": "TEST_ONLY", "qmt_dir": str(manager.qmt_dir)}
388
+ monkeypatch.setattr(web, "WEB_CONFIG", SimpleNamespace(account_configs=lambda: {"test": row}))
389
+ monkeypatch.setattr(web, "QMT_PYTHON_RUNTIME_JOBS", {})
390
+ monkeypatch.setattr(web.threading.Thread, "start", lambda self: None)
391
+ monkeypatch.setattr(web, "emit_download_progress", lambda *a, **kw: None)
392
+ monkeypatch.setattr(QmtPythonRuntime, "_qmt_processes", lambda self: [])
393
+ monkeypatch.setattr(QmtPythonRuntime, "restart_qmt", lambda self: pytest.fail("must not bypass managed restart"))
394
+ restart = {"configured": False, "error": "deployment write failed"} if restart_failed else {
395
+ "configured": True, "pid": 456, "login_state": "logging_in"}
396
+ monkeypatch.setattr(web, "qmt_auto_login_restart_for_account", lambda *a, **kw: restart)
397
+ body = {"qmt_dir": str(manager.qmt_dir), "account_id": "TEST_ONLY", "runtime": "py36",
398
+ "reuse_installed": True, "restart": True, "job_id": "managed-restart"}
399
+ web.start_qmt_python_runtime_job(body)
400
+ web._run_qmt_python_runtime_job(body["job_id"], body)
401
+ job = web._qmt_python_job_snapshot(body["job_id"])
402
+ assert job["state"] == ("error" if restart_failed else "done")
403
+ if restart_failed:
404
+ assert "deployment write failed" in job["error"]
405
+ else:
406
+ assert job["result"]["restarted"] == restart
407
+ assert manager.binding_status()["state"] == "ready"
408
+
409
+
410
+ def test_runtime_bootstrap_starts_stops_and_starts_managed_qmt(tmp_path, monkeypatch):
411
+ from types import SimpleNamespace
412
+ import cfquant_web_server as web
413
+
414
+ row = {
415
+ "account_key": "bootstrap-account",
416
+ "account_id": "55009948",
417
+ "qmt_dir": str(tmp_path / "qmt"),
418
+ "qmt_strategy": {"enabled": True},
419
+ "qmt_auto_login": {"enabled": True, "auto_fill_credentials": True},
420
+ }
421
+ manager = SimpleNamespace(
422
+ binding_status=lambda account_id=None: {"state": "account_config_missing"},
423
+ switch_account=lambda runtime, account_id=None: {"runtime": runtime, "account_id": account_id},
424
+ )
425
+ paths = {"bin_dir": str(tmp_path / "qmt" / "bin.x64"), "exe_path": "fixture.exe"}
426
+ monkeypatch.setattr(web, "_qmt_auto_login_paths", lambda _: paths)
427
+ monkeypatch.setattr(web, "_qmt_wait_for_runtime_bootstrap_login",
428
+ lambda row, paths: {"login_succeeded": True, "login_state": "logged_in"})
429
+ monkeypatch.setattr(web, "_qmt_auto_login_stop_xtitclient",
430
+ lambda *args, **kwargs: {"exit_confirmed": True})
431
+ calls = []
432
+
433
+ def restart(account_row, settings, reason):
434
+ calls.append(reason)
435
+ if reason.endswith("first"):
436
+ return {"configured": True, "pid": 101, "strategy_deploy_deferred": True}
437
+ return {"configured": True, "pid": 202, "strategy_deploy_deferred": False}
438
+
439
+ monkeypatch.setattr(web, "qmt_auto_login_restart_for_account", restart)
440
+ result = web._qmt_python_runtime_bootstrap_restart(
441
+ row, row["qmt_auto_login"], manager, "py36", row["account_id"],
442
+ body={"allow_missing_account_config": True},
443
+ )
444
+
445
+ assert calls == ["python-runtime-bootstrap-first", "python-runtime-bootstrap-final"]
446
+ assert result["bootstrap_completed"] is True
447
+ assert result["pid"] == 202
448
+ assert result["runtime_switch"]["runtime"] == "py36"
@@ -488,6 +488,25 @@ def test_no_account_binding_is_never_fabricated(deployment):
488
488
  _account_binding(document, ACCOUNT, "STOCK", ACCOUNT_KEY)
489
489
 
490
490
 
491
+ def test_first_login_can_start_and_next_start_prepares_real_binding(deployment):
492
+ manager, row, infos, running, root = deployment
493
+ config = root / "config" / "indexUserConfig.xml"
494
+ config.write_text('<ICUserConfigFile><FormulaCatalog/><strategyTrade/></ICUserConfigFile>', encoding="utf-8")
495
+ manager.configure(row, infos)
496
+ prepared = manager.prepare_for_start(root)
497
+ assert prepared["waiting_account"]
498
+ assert not models(root)
499
+ finish_import(manager, root)
500
+ auth = root / "userdata" / "users" / "test_user" / "authAndConfig.xml"
501
+ auth.parent.mkdir(parents=True)
502
+ auth.write_text('<TTAuthAndConfigFile><TTAlgoConfigFile><AccountAuth key="%s" strategys="1"/>'
503
+ '</TTAlgoConfigFile></TTAuthAndConfigFile>' % ACCOUNT_KEY, encoding="utf-8")
504
+ assert not manager.prepare_for_start(root)["waiting_account"]
505
+ role = next(iter(manager.jobs.values()))["roles"][0]
506
+ assert role["model_prepared"]
507
+ assert models(root)[role["name"]].getAttribute("m_strAccountKey") == ACCOUNT_KEY
508
+
509
+
491
510
  def test_first_start_has_a_credit_model_even_without_an_existing_model(deployment):
492
511
  manager, row, infos, running, root = deployment
493
512
  config = root / "config" / "indexUserConfig.xml"
@@ -1,5 +1,5 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """cfquant core version metadata."""
3
3
 
4
- __version__ = "0.2.68"
5
- WEB_VERSION = "web_20261111_02"
4
+ __version__ = "0.2.70"
5
+ WEB_VERSION = "web_20261112_01"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cfquant
3
- Version: 0.2.68
3
+ Version: 0.2.70
4
4
  Summary: QMT local bridge and xtquant-compatible Python client.
5
5
  License: MIT
6
6
  Requires-Python: >=3.8