cfquant 0.2.68__tar.gz → 0.2.71__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 (311) hide show
  1. {cfquant-0.2.68 → cfquant-0.2.71}/PKG-INFO +1 -1
  2. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_python_runtime.py +181 -54
  3. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_strategy_deploy.py +11 -3
  4. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_binding_python_runtime_browser.py +73 -0
  5. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_auto_login_integration.py +32 -0
  6. cfquant-0.2.71/cfquant/tests/test_qmt_python_mirror.py +181 -0
  7. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_python_runtime.py +139 -0
  8. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_strategy_deploy.py +19 -0
  9. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/version.py +2 -2
  10. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant.egg-info/PKG-INFO +1 -1
  11. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant.egg-info/SOURCES.txt +1 -0
  12. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant_web_server.py +163 -40
  13. {cfquant-0.2.68 → cfquant-0.2.71}/pyproject.toml +1 -1
  14. {cfquant-0.2.68 → cfquant-0.2.71}/qmt_scripts/CFQUANT_LITE.py +1 -1
  15. {cfquant-0.2.68 → cfquant-0.2.71}/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
  16. {cfquant-0.2.68 → cfquant-0.2.71}/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
  17. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/app.js +66 -44
  18. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/index.html +2 -2
  19. {cfquant-0.2.68 → cfquant-0.2.71}/LICENSE +0 -0
  20. {cfquant-0.2.68 → cfquant-0.2.71}/LTtx/__init__.py +0 -0
  21. {cfquant-0.2.68 → cfquant-0.2.71}/LTtx/tx/Config.txt +0 -0
  22. {cfquant-0.2.68 → cfquant-0.2.71}/LTtx/tx/LTtx_server.py +0 -0
  23. {cfquant-0.2.68 → cfquant-0.2.71}/LTtx/tx/__init__.py +0 -0
  24. {cfquant-0.2.68 → cfquant-0.2.71}/LTtx/tx/tx.py +0 -0
  25. {cfquant-0.2.68 → cfquant-0.2.71}/MANIFEST.in +0 -0
  26. {cfquant-0.2.68 → cfquant-0.2.71}/README.md +0 -0
  27. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/__init__.py +0 -0
  28. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/_editable_install.py +0 -0
  29. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/account_routing.py +0 -0
  30. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/algo.py +0 -0
  31. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/algo_broker.py +0 -0
  32. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/batch_orders.py +0 -0
  33. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/build_info.py +0 -0
  34. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/callback_dispatcher.py +0 -0
  35. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/cftrader.py +0 -0
  36. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/channels.py +0 -0
  37. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/cli.py +0 -0
  38. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/client.py +0 -0
  39. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/conditional_orders.py +0 -0
  40. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/config.py +0 -0
  41. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/diagnostic_context.py +0 -0
  42. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/diagnostics.py +0 -0
  43. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/flow_control.py +0 -0
  44. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/hub.py +0 -0
  45. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/level2.py +0 -0
  46. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/log_management.py +0 -0
  47. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/logging_i18n.py +0 -0
  48. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/management.py +0 -0
  49. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/monitor_notifications.py +0 -0
  50. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/monitor_performance.py +0 -0
  51. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/normal_bridge.py +0 -0
  52. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/notify.py +0 -0
  53. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/order_identity.py +0 -0
  54. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/order_meta.py +0 -0
  55. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/pipe_bridge.py +0 -0
  56. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/pipe_client.py +0 -0
  57. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/pipe_hub.py +0 -0
  58. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/pipe_transport.py +0 -0
  59. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/planning_broker.py +0 -0
  60. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/position_sizing.py +0 -0
  61. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/protocol.py +0 -0
  62. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_binding_defaults.py +0 -0
  63. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_binding_settings.py +0 -0
  64. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_bridge.py +0 -0
  65. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_component_agent.js +0 -0
  66. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_component_login.py +0 -0
  67. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_credentials.py +0 -0
  68. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_dependencies.py +0 -0
  69. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_inner.py +0 -0
  70. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_inner_catalog.py +0 -0
  71. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_inner_runtime.py +0 -0
  72. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_mac_agent.js +0 -0
  73. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_native_files.py +0 -0
  74. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_native_worker.py +0 -0
  75. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_pipe_runtime.py +0 -0
  76. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_strategy_agent.js +0 -0
  77. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_strategy_package.py +0 -0
  78. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_strategy_recovery.py +0 -0
  79. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_strategy_runtime.py +0 -0
  80. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_update.py +0 -0
  81. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_vm_agent.js +0 -0
  82. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_vm_discovery.py +0 -0
  83. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_vm_runtime.py +0 -0
  84. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_vm_settings.py +0 -0
  85. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_vm_worker.py +0 -0
  86. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/quote_history.py +0 -0
  87. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/quote_leases.py +0 -0
  88. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/quote_service.py +0 -0
  89. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/remote_adapter.py +0 -0
  90. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/remote_management.py +0 -0
  91. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/remote_supervisor.py +0 -0
  92. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/rpc_timing.py +0 -0
  93. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/runtime_report.py +0 -0
  94. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/shared_quotes.py +0 -0
  95. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/smart_orders.py +0 -0
  96. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/stock_connect.py +0 -0
  97. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/subscription_bridge.py +0 -0
  98. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/subscription_client.py +0 -0
  99. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/subscription_manager.py +0 -0
  100. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/subscription_store.py +0 -0
  101. {cfquant-0.2.68 → cfquant-0.2.71}/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
  102. {cfquant-0.2.68 → cfquant-0.2.71}/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
  103. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/12_/350/256/242/345/215/225/345/244/207/346/263/250/346/265/213/350/257/225.py" +0 -0
  104. {cfquant-0.2.68 → cfquant-0.2.71}/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
  105. {cfquant-0.2.68 → cfquant-0.2.71}/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
  106. {cfquant-0.2.68 → cfquant-0.2.71}/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
  107. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/16_/345/220/257/345/212/250/345/256/211/350/243/205/346/265/213/350/257/225.py" +0 -0
  108. {cfquant-0.2.68 → cfquant-0.2.71}/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
  109. {cfquant-0.2.68 → cfquant-0.2.71}/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
  110. {cfquant-0.2.68 → cfquant-0.2.71}/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
  111. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/1_/350/241/214/346/203/205/346/216/245/346/224/266/346/265/213/350/257/225.py" +0 -0
  112. {cfquant-0.2.68 → cfquant-0.2.71}/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
  113. {cfquant-0.2.68 → cfquant-0.2.71}/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
  114. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/22_/350/277/236/346/216/245/350/257/212/346/226/255/346/265/213/350/257/225.py" +0 -0
  115. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/23_/347/244/272/344/276/213/351/205/215/347/275/256/346/265/213/350/257/225.py" +0 -0
  116. {cfquant-0.2.68 → cfquant-0.2.71}/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
  117. {cfquant-0.2.68 → cfquant-0.2.71}/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
  118. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/26_/345/205/250/346/216/250/350/256/242/351/230/205/346/265/213/350/257/225.py" +0 -0
  119. {cfquant-0.2.68 → cfquant-0.2.71}/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
  120. {cfquant-0.2.68 → cfquant-0.2.71}/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
  121. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/29_Level2/350/241/214/346/203/205/346/265/213/350/257/225.py" +0 -0
  122. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/2_/346/225/260/346/215/256/350/216/267/345/217/226/346/265/213/350/257/225.py" +0 -0
  123. {cfquant-0.2.68 → cfquant-0.2.71}/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
  124. {cfquant-0.2.68 → cfquant-0.2.71}/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
  125. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/32_/345/274/202/346/255/245/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  126. {cfquant-0.2.68 → cfquant-0.2.71}/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
  127. {cfquant-0.2.68 → cfquant-0.2.71}/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
  128. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/3_/346/225/260/346/215/256/344/270/213/350/275/275/346/265/213/350/257/225.py" +0 -0
  129. {cfquant-0.2.68 → cfquant-0.2.71}/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
  130. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/5_/347/234/237/345/256/236/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  131. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/6_/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
  132. {cfquant-0.2.68 → cfquant-0.2.71}/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
  133. {cfquant-0.2.68 → cfquant-0.2.71}/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
  134. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/9_/345/217/257/346/222/244/350/256/242/345/215/225/346/265/213/350/257/225.py" +0 -0
  135. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/_helpers.py +0 -0
  136. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/qmt_vm_native_fixture.js +0 -0
  137. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/readme.md +0 -0
  138. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_account_query_timing.py +0 -0
  139. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_account_subscription_probe.py +0 -0
  140. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_activity_log_browser.py +0 -0
  141. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_advanced_parity.py +0 -0
  142. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_algo.py +0 -0
  143. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_algo_broker.py +0 -0
  144. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_api_tester_browser.py +0 -0
  145. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_api_tester_web.py +0 -0
  146. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_auto_login_app.py +0 -0
  147. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
  148. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_binding_live_refresh_browser.py +0 -0
  149. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_binding_runtime_browser.py +0 -0
  150. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_binding_vm_browser.py +0 -0
  151. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_binding_webhook_browser.py +0 -0
  152. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_callback_recovery.py +0 -0
  153. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_callback_requests.py +0 -0
  154. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_cftrader.py +0 -0
  155. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_cftrader_tutorial.py +0 -0
  156. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_cftrader_web.py +0 -0
  157. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_credit_compat.py +0 -0
  158. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_dataframe_encoding.py +0 -0
  159. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_diagnostic_context.py +0 -0
  160. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_history_download_memory.py +0 -0
  161. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_json_serialization.py +0 -0
  162. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_latency_flow_control.py +0 -0
  163. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_lite_account_routing.py +0 -0
  164. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_lite_deployment_paths.py +0 -0
  165. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_lite_parity.py +0 -0
  166. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_lite_qmt_dispatch.py +0 -0
  167. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_local_trading_calendar.py +0 -0
  168. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_log_management.py +0 -0
  169. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_management.py +0 -0
  170. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_management_batch.py +0 -0
  171. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_market_data_fallback.py +0 -0
  172. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_market_timing.py +0 -0
  173. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_monitor_notifications.py +0 -0
  174. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_monitor_performance.py +0 -0
  175. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_navigation_browser.py +0 -0
  176. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_notify.py +0 -0
  177. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_order_identity.py +0 -0
  178. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_order_time_callback.py +0 -0
  179. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_pipe_shutdown.py +0 -0
  180. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_project_updater.py +0 -0
  181. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_python_api_reference.py +0 -0
  182. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_auto_login_browser.py +0 -0
  183. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_binding_defaults.py +0 -0
  184. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_binding_settings.py +0 -0
  185. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_component_login.py +0 -0
  186. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_dependencies.py +0 -0
  187. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_fileless.py +0 -0
  188. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_inner.py +0 -0
  189. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_inner_reference.py +0 -0
  190. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_inner_web.py +0 -0
  191. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_native_files.py +0 -0
  192. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_native_reuse.py +0 -0
  193. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_native_worker.py +0 -0
  194. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_package_import.py +0 -0
  195. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_strategy_browser.py +0 -0
  196. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_strategy_capabilities.py +0 -0
  197. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_strategy_recovery.py +0 -0
  198. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_strategy_web.py +0 -0
  199. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_update.py +0 -0
  200. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_vm_discovery.py +0 -0
  201. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_vm_native.py +0 -0
  202. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_vm_runtime.py +0 -0
  203. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_vm_settings.py +0 -0
  204. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_quote_latency.py +0 -0
  205. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_quote_latency_encoding.py +0 -0
  206. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_quote_service.py +0 -0
  207. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_remote_management.py +0 -0
  208. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_remote_management_browser.py +0 -0
  209. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_remote_version_check.py +0 -0
  210. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
  211. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_shared_quotes.py +0 -0
  212. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_shared_quotes_generated.py +0 -0
  213. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_smart_orders.py +0 -0
  214. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_smart_orders_api.py +0 -0
  215. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_smart_orders_docs.py +0 -0
  216. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_split_order_identity.py +0 -0
  217. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_startup_recovery.py +0 -0
  218. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_stock_connect.py +0 -0
  219. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_stock_connect_browser.py +0 -0
  220. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_strategy_recovery_api.py +0 -0
  221. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_subscription_congestion.py +0 -0
  222. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_subscription_persistence.py +0 -0
  223. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_table_conversion.py +0 -0
  224. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_trade_strategy_callback.py +0 -0
  225. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_trader_subscription_recovery.py +0 -0
  226. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_trading_dates.py +0 -0
  227. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_trading_monitor.py +0 -0
  228. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_trading_monitor_browser.py +0 -0
  229. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_tutorial_reader.py +0 -0
  230. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_tx_auto_import.py +0 -0
  231. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_tx_shutdown.py +0 -0
  232. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_version_check.py +0 -0
  233. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_version_check_browser.py +0 -0
  234. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tools/THIRD_PARTY.md +0 -0
  235. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tools/upx/COPYING +0 -0
  236. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tools/upx/LICENSE +0 -0
  237. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tools/upx/NEWS +0 -0
  238. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tools/upx/README +0 -0
  239. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tools/upx/THANKS.txt +0 -0
  240. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tools/upx/upx-doc.html +0 -0
  241. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tools/upx/upx-doc.txt +0 -0
  242. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tools/upx/upx.1 +0 -0
  243. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tools/upx/upx.exe +0 -0
  244. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/trading_calendar.py +0 -0
  245. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/trading_monitor.py +0 -0
  246. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tx.py +0 -0
  247. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tx_trade_bridge.py +0 -0
  248. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/webhook.py +0 -0
  249. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/xtconstant.py +0 -0
  250. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/xtdata.py +0 -0
  251. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/xttrader.py +0 -0
  252. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/xttype.py +0 -0
  253. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant.egg-info/dependency_links.txt +0 -0
  254. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant.egg-info/entry_points.txt +0 -0
  255. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant.egg-info/requires.txt +0 -0
  256. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant.egg-info/top_level.txt +0 -0
  257. {cfquant-0.2.68 → cfquant-0.2.71}/cfquant_pipe_hub.py +0 -0
  258. {cfquant-0.2.68 → cfquant-0.2.71}/qmt_scripts/CFQUANT.py +0 -0
  259. {cfquant-0.2.68 → cfquant-0.2.71}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +0 -0
  260. {cfquant-0.2.68 → cfquant-0.2.71}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
  261. {cfquant-0.2.68 → cfquant-0.2.71}/qmt_scripts/__init__.py +0 -0
  262. {cfquant-0.2.68 → cfquant-0.2.71}/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
  263. {cfquant-0.2.68 → cfquant-0.2.71}/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
  264. {cfquant-0.2.68 → cfquant-0.2.71}/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
  265. {cfquant-0.2.68 → cfquant-0.2.71}/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
  266. {cfquant-0.2.68 → cfquant-0.2.71}/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
  267. {cfquant-0.2.68 → cfquant-0.2.71}/requirements.txt +0 -0
  268. {cfquant-0.2.68 → cfquant-0.2.71}/setup.cfg +0 -0
  269. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/__init__.py +0 -0
  270. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/activity-log.js +0 -0
  271. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/api-tester.js +0 -0
  272. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/avatars/amber-pulse.svg +0 -0
  273. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/avatars/copper-grid.svg +0 -0
  274. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/avatars/market-blue.svg +0 -0
  275. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/avatars/rose-circuit.svg +0 -0
  276. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/avatars/signal-green.svg +0 -0
  277. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/avatars/slate-wave.svg +0 -0
  278. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/avatars/teal-orbit.svg +0 -0
  279. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/avatars/violet-node.svg +0 -0
  280. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/cfquant-architecture.html +0 -0
  281. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/cftrader-api-data.js +0 -0
  282. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/docs.js +0 -0
  283. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/favicon.svg +0 -0
  284. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
  285. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
  286. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
  287. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
  288. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
  289. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
  290. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
  291. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
  292. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
  293. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
  294. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
  295. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
  296. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
  297. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
  298. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
  299. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
  300. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
  301. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
  302. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/navigation.js +0 -0
  303. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/notify-api-data.js +0 -0
  304. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/python-api-data.js +0 -0
  305. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/python-api.js +0 -0
  306. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/qmt-inner-api-data.js +0 -0
  307. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/remote-management.js +0 -0
  308. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/smart-order-api-data.js +0 -0
  309. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/styles.css +0 -0
  310. {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/trading-monitor.js +0 -0
  311. {cfquant-0.2.68 → cfquant-0.2.71}/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.71
4
4
  Summary: QMT local bridge and xtquant-compatible Python client.
5
5
  License: MIT
6
6
  Requires-Python: >=3.8
@@ -15,7 +15,9 @@ from __future__ import annotations
15
15
  from dataclasses import dataclass, asdict
16
16
  import configparser
17
17
  import hashlib
18
+ import http.client
18
19
  import json
20
+ import logging
19
21
  import os
20
22
  from pathlib import Path
21
23
  import re
@@ -26,6 +28,7 @@ import time
26
28
  from typing import Dict, Optional
27
29
  import urllib.error
28
30
  import urllib.request
31
+ from urllib.parse import urlencode
29
32
  import zipfile
30
33
 
31
34
 
@@ -33,6 +36,16 @@ class QmtPythonRuntimeError(RuntimeError):
33
36
  """A download, validation, activation, or restart operation failed."""
34
37
 
35
38
 
39
+ class _DownloadSourceError(QmtPythonRuntimeError):
40
+ """A remote failure that permits trying another copy of the same package."""
41
+
42
+
43
+ class _NoMirrorRedirect(urllib.request.HTTPRedirectHandler):
44
+ def redirect_request(self, req, fp, code, msg, headers, newurl):
45
+ # Never forward the shared mirror key to a redirected host.
46
+ return None
47
+
48
+
36
49
  @dataclass(frozen=True)
37
50
  class QmtPythonPackage:
38
51
  runtime: str
@@ -41,6 +54,8 @@ class QmtPythonPackage:
41
54
  filename: str
42
55
  md5: str
43
56
  force_update: bool = False
57
+ sha256: str = ""
58
+ size: int = 0
44
59
 
45
60
  def as_dict(self):
46
61
  return asdict(self)
@@ -67,6 +82,10 @@ _RUNTIME_ALIASES = {"36": "py36", "3.6": "py36", "python36": "py36",
67
82
  _PY312_MIN_QMT_VERSION = (2, 1, 33, 0)
68
83
  _CONFIG_LOCKS: Dict[str, threading.RLock] = {}
69
84
  _CONFIG_LOCKS_GUARD = threading.Lock()
85
+ _MIRROR_BASE = "https://cfquant.org/api/qmt/python-runtime/"
86
+ _MIRROR_KEY_HEADER = "X-CFQuant-Runtime-Key"
87
+ _MIRROR_KEY = "cfq-runtime-v1-4859bf5af16942e38b30f128bd540714"
88
+ _LOG = logging.getLogger(__name__)
70
89
 
71
90
 
72
91
  def _canonical_runtime(runtime: str) -> str:
@@ -118,6 +137,10 @@ class QmtPythonRuntime:
118
137
  self.staging_dir = self.state_dir / "staging"
119
138
  self.backup_dir = self.state_dir / "backups"
120
139
  self._opener = opener or urllib.request.build_opener(urllib.request.ProxyHandler({}))
140
+ self._mirror_opener = opener or urllib.request.build_opener(
141
+ urllib.request.ProxyHandler({}), _NoMirrorRedirect())
142
+ self._mirror_cache = None
143
+ self._mirror_checked_at = float("-inf")
121
144
  self._lock = self._lock_for(str(self.bin_dir))
122
145
 
123
146
  @staticmethod
@@ -131,10 +154,50 @@ class QmtPythonRuntime:
131
154
 
132
155
  def _request(self, url, *, headers=None, timeout=None):
133
156
  request = urllib.request.Request(url, headers=headers or {}, method="GET")
134
- return self._opener.open(request, timeout=timeout or self.timeout)
157
+ opener = self._mirror_opener if url.startswith(_MIRROR_BASE) else self._opener
158
+ return opener.open(request, timeout=timeout or self.timeout)
159
+
160
+ def _mirror_status(self):
161
+ if time.monotonic() - self._mirror_checked_at < 5:
162
+ return self._mirror_cache
163
+ self._mirror_checked_at = time.monotonic()
164
+ self._mirror_cache = None
165
+ try:
166
+ with self._request(_MIRROR_BASE + "status", headers={_MIRROR_KEY_HEADER: _MIRROR_KEY},
167
+ timeout=min(self.timeout, 5)) as response:
168
+ data = json.loads(response.read(65537).decode("utf-8"))
169
+ if (not data.get("ok") or data.get("schema") != 1 or not isinstance(data.get("packages"), dict)
170
+ or not isinstance(data.get("busy"), bool)):
171
+ raise ValueError("invalid mirror status")
172
+ self._mirror_cache = data
173
+ except (OSError, ValueError, AttributeError, http.client.HTTPException) as exc:
174
+ _LOG.info("QMT runtime mirror status unavailable: %s", exc)
175
+ return self._mirror_cache
176
+
177
+ @staticmethod
178
+ def _package_from_mirror(data, runtime):
179
+ try:
180
+ item = data["packages"][runtime]
181
+ if (not re.fullmatch(r"[A-Za-z0-9_.-]+\.zip", item["filename"])
182
+ or not re.fullmatch(r"[0-9a-f]{32}", item["md5"])
183
+ or not re.fullmatch(r"[0-9a-f]{64}", item["sha256"])
184
+ or not item["url"].startswith(("http://", "https://"))
185
+ or int(item["size"]) <= 0):
186
+ return None
187
+ return QmtPythonPackage(runtime, item["version"], item["url"], item["filename"],
188
+ item["md5"], bool(item.get("force_update")),
189
+ item["sha256"], int(item["size"]))
190
+ except (KeyError, TypeError, ValueError, AttributeError):
191
+ return None
135
192
 
136
193
  def fetch_package(self, runtime="py312") -> QmtPythonPackage:
137
194
  runtime = _canonical_runtime(runtime)
195
+ mirrored = self._package_from_mirror(self._mirror_status(), runtime)
196
+ if mirrored:
197
+ return mirrored
198
+ return self._fetch_upstream_package(runtime)
199
+
200
+ def _fetch_upstream_package(self, runtime):
138
201
  cfg_url = "http://download.thinktrader.net:8800/download/" + _CONFIG_NAMES[runtime]
139
202
  try:
140
203
  with self._request(cfg_url) as response:
@@ -350,68 +413,127 @@ class QmtPythonRuntime:
350
413
  destination = self.download_dir / package.filename
351
414
  partial = destination.with_suffix(destination.suffix + ".part")
352
415
  with self._lock:
353
- if destination.is_file() and self._md5(destination) == package.md5:
416
+ if destination.is_file() and self._archive_matches(destination, package):
354
417
  _callback(progress_callback, QmtPythonProgress(
355
418
  runtime, "download_verified", destination.stat().st_size,
356
419
  destination.stat().st_size, 100, 0, 0, "download already verified"))
357
420
  return destination
358
- existing = partial.stat().st_size if partial.exists() and keep_existing else 0
359
- if not existing and not keep_existing and partial.exists():
421
+ identity_file = partial.with_suffix(partial.suffix + ".identity")
422
+ identity = package.md5 + ":" + package.sha256
423
+ previous = identity_file.read_text("ascii") if identity_file.exists() else ""
424
+ if partial.exists() and (not keep_existing or previous != identity):
360
425
  partial.unlink()
361
- headers = {"Accept-Encoding": "identity"}
362
- if existing:
363
- headers["Range"] = "bytes=%d-" % existing
364
- try:
365
- response = self._request(package.url, headers=headers)
366
- except (OSError, urllib.error.URLError) as exc:
367
- raise QmtPythonRuntimeError("cannot download QMT runtime: %s" % exc) from exc
368
- status = getattr(response, "status", response.getcode())
369
- if status == 206 and existing:
370
- mode, done = "ab", existing
371
- elif status == 200:
372
- mode, done = "wb", 0
426
+ identity_file.write_text(identity, encoding="ascii")
427
+ self._check_cancel(cancel_event)
428
+ snapshot = self._mirror_status()
429
+ mirrored = self._package_from_mirror(snapshot, runtime)
430
+ mirror_url = _MIRROR_BASE + "download?" + urlencode(dict(runtime=runtime, md5=package.md5))
431
+ sources = []
432
+ if mirrored and mirrored.md5 == package.md5 and not snapshot["busy"]:
433
+ sources.append(("官网", mirror_url))
434
+ sources.extend([("讯投", package.url), ("官网(失败回退)", mirror_url + "&force=1")])
435
+ errors = []
436
+ for source, url in sources:
437
+ self._check_cancel(cancel_event)
438
+ _LOG.info("QMT runtime download source=%s runtime=%s file=%s", source, runtime, package.filename)
439
+ _callback(progress_callback, QmtPythonProgress(runtime, "download", message="正在通过%s下载运行库" % source))
440
+ try:
441
+ self._download_source(package, url, partial, source, progress_callback, cancel_event)
442
+ break
443
+ except _DownloadSourceError as exc:
444
+ errors.append("%s: %s" % (source, exc))
445
+ _LOG.warning("QMT runtime source failed source=%s runtime=%s error=%s", source, runtime, exc)
373
446
  else:
374
- response.close()
375
- raise QmtPythonRuntimeError("unexpected QMT download HTTP status: %s" % status)
376
- total_header = response.headers.get("Content-Range", "").split("/")[-1]
377
- total = int(total_header) if total_header.isdigit() else int(response.headers.get("Content-Length") or 0) + done
378
- started = time.monotonic()
379
- _callback(progress_callback, QmtPythonProgress(runtime, "download", done, total,
380
- (done / total * 100) if total else 0,
381
- 0, None, "downloading"))
382
- try:
383
- with response, open(partial, mode) as stream:
384
- while True:
385
- if cancel_event is not None and cancel_event.is_set():
386
- raise QmtPythonRuntimeError("QMT runtime download cancelled")
387
- chunk = response.read(1024 * 1024)
388
- if not chunk:
389
- break
390
- stream.write(chunk)
391
- done += len(chunk)
392
- elapsed = max(time.monotonic() - started, 0.001)
393
- speed = (done - (existing if mode == "ab" else 0)) / elapsed
394
- eta = ((total - done) / speed) if speed > 0 and total else None
395
- _callback(progress_callback, QmtPythonProgress(
396
- runtime, "download", done, total,
397
- (done / total * 100) if total else 0, speed, eta, "downloading"))
398
- stream.flush()
399
- os.fsync(stream.fileno())
400
- except QmtPythonRuntimeError:
401
- raise
402
- except OSError as exc:
403
- raise QmtPythonRuntimeError("cannot write QMT runtime download: %s" % exc) from exc
404
- actual = self._md5(partial)
405
- if actual != package.md5:
406
- raise QmtPythonRuntimeError("QMT runtime MD5 mismatch: expected %s, got %s" %
407
- (package.md5, actual))
447
+ raise QmtPythonRuntimeError("QMT runtime download failed; " + "; ".join(errors))
408
448
  os.replace(partial, destination)
449
+ identity_file.unlink()
450
+ size = destination.stat().st_size
409
451
  _callback(progress_callback, QmtPythonProgress(runtime, "download_verified",
410
- total or destination.stat().st_size,
411
- total or destination.stat().st_size,
452
+ size, size,
412
453
  100, 0, 0, "download verified"))
413
454
  return destination
414
455
 
456
+ @staticmethod
457
+ def _check_cancel(event):
458
+ if event is not None and event.is_set():
459
+ raise QmtPythonRuntimeError("QMT runtime download cancelled")
460
+
461
+ def _archive_matches(self, path, package):
462
+ if package.size and path.stat().st_size != package.size:
463
+ return False
464
+ if self._md5(path) != package.md5:
465
+ return False
466
+ if package.sha256:
467
+ digest = hashlib.sha256()
468
+ with path.open("rb") as stream:
469
+ for chunk in iter(lambda: stream.read(1024 * 1024), b""):
470
+ digest.update(chunk)
471
+ return digest.hexdigest() == package.sha256
472
+ return True
473
+
474
+ def _download_source(self, package, url, partial, source, callback, cancel_event):
475
+ existing = partial.stat().st_size if partial.exists() else 0
476
+ if existing and self._archive_matches(partial, package):
477
+ return
478
+ if package.size and existing >= package.size:
479
+ partial.unlink()
480
+ existing = 0
481
+ headers = {"Accept-Encoding": "identity"}
482
+ if url.startswith(_MIRROR_BASE):
483
+ headers[_MIRROR_KEY_HEADER] = _MIRROR_KEY
484
+ if existing:
485
+ headers["Range"] = "bytes=%d-" % existing
486
+ try:
487
+ response = self._request(url, headers=headers)
488
+ except (OSError, http.client.HTTPException) as exc:
489
+ if isinstance(exc, urllib.error.HTTPError) and exc.code == 416:
490
+ partial.unlink(missing_ok=True)
491
+ raise _DownloadSourceError("cannot connect: %s" % exc) from exc
492
+ with response:
493
+ status = response.getcode()
494
+ if status == 206 and existing:
495
+ match = re.fullmatch(r"bytes (\d+)-(\d+)/(\d+)", response.headers.get("Content-Range", ""))
496
+ if not match or int(match[1]) != existing or int(match[2]) != int(match[3]) - 1:
497
+ raise _DownloadSourceError("invalid resume Content-Range")
498
+ mode, done, total = "ab", existing, int(match[3])
499
+ elif status == 200:
500
+ mode, done = "wb", 0
501
+ try:
502
+ total = int(response.headers.get("Content-Length") or package.size)
503
+ except ValueError as exc:
504
+ raise _DownloadSourceError("invalid Content-Length") from exc
505
+ else:
506
+ raise _DownloadSourceError("unexpected HTTP status: %s" % status)
507
+ if package.size and total and total != package.size:
508
+ raise _DownloadSourceError("package size differs from manifest")
509
+ started, initial = time.monotonic(), done
510
+ with partial.open(mode) as stream:
511
+ while True:
512
+ self._check_cancel(cancel_event)
513
+ try:
514
+ chunk = response.read(1024 * 1024)
515
+ except (OSError, http.client.HTTPException) as exc:
516
+ raise _DownloadSourceError("connection interrupted: %s" % exc) from exc
517
+ if not chunk:
518
+ break
519
+ stream.write(chunk) # Local disk failures must not trigger another download.
520
+ done += len(chunk)
521
+ if total and done > total:
522
+ break
523
+ speed = (done - initial) / max(time.monotonic() - started, 0.001)
524
+ _callback(callback, QmtPythonProgress(
525
+ package.runtime, "download", done, total, (done / total * 100) if total else 0,
526
+ speed, (total - done) / speed if total and speed else None,
527
+ "正在通过%s下载运行库" % source))
528
+ stream.flush()
529
+ os.fsync(stream.fileno())
530
+ self._check_cancel(cancel_event)
531
+ if total and done < total:
532
+ raise _DownloadSourceError("download truncated: %d/%d" % (done, total))
533
+ if not self._archive_matches(partial, package):
534
+ partial.unlink()
535
+ raise _DownloadSourceError("QMT runtime MD5/SHA-256 mismatch")
536
+
415
537
  @staticmethod
416
538
  def _safe_extract(archive, destination):
417
539
  destination = Path(destination).resolve()
@@ -442,7 +564,7 @@ class QmtPythonRuntime:
442
564
  runtime = _canonical_runtime(runtime)
443
565
  package = package or self.fetch_package(runtime)
444
566
  archive = self.download_dir / package.filename
445
- if not archive.is_file() or self._md5(archive) != package.md5:
567
+ if not archive.is_file() or not self._archive_matches(archive, package):
446
568
  raise QmtPythonRuntimeError("verified QMT runtime archive is missing")
447
569
  with self._lock:
448
570
  self.staging_dir.mkdir(parents=True, exist_ok=True)
@@ -655,7 +777,12 @@ class QmtPythonRuntime:
655
777
  if (account_id or self.account_id) else None
656
778
  )
657
779
  finally:
658
- if restart and (not deferred_account_config or was_running):
780
+ # A managed Web binding can supply a bootstrap callback that starts
781
+ # QMT, waits for the first login to create Config.xml, and then
782
+ # performs the final runtime switch/restart. Keep the standalone
783
+ # API's old safe behavior (leave a brand-new, unconfigured QMT
784
+ # stopped) when no callback was supplied.
785
+ if restart and (not deferred_account_config or was_running or restart_callback):
659
786
  _callback(progress_callback, QmtPythonProgress(runtime, "restarting", message="正在重新启动 QMT"))
660
787
  if restart_callback:
661
788
  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