cfquant 0.2.70__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.70 → cfquant-0.2.71}/PKG-INFO +1 -1
  2. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_python_runtime.py +175 -53
  3. cfquant-0.2.71/cfquant/tests/test_qmt_python_mirror.py +181 -0
  4. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/version.py +2 -2
  5. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant.egg-info/PKG-INFO +1 -1
  6. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant.egg-info/SOURCES.txt +1 -0
  7. {cfquant-0.2.70 → cfquant-0.2.71}/pyproject.toml +1 -1
  8. {cfquant-0.2.70 → cfquant-0.2.71}/qmt_scripts/CFQUANT_LITE.py +1 -1
  9. {cfquant-0.2.70 → 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
  10. {cfquant-0.2.70 → 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
  11. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/app.js +1 -1
  12. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/index.html +2 -2
  13. {cfquant-0.2.70 → cfquant-0.2.71}/LICENSE +0 -0
  14. {cfquant-0.2.70 → cfquant-0.2.71}/LTtx/__init__.py +0 -0
  15. {cfquant-0.2.70 → cfquant-0.2.71}/LTtx/tx/Config.txt +0 -0
  16. {cfquant-0.2.70 → cfquant-0.2.71}/LTtx/tx/LTtx_server.py +0 -0
  17. {cfquant-0.2.70 → cfquant-0.2.71}/LTtx/tx/__init__.py +0 -0
  18. {cfquant-0.2.70 → cfquant-0.2.71}/LTtx/tx/tx.py +0 -0
  19. {cfquant-0.2.70 → cfquant-0.2.71}/MANIFEST.in +0 -0
  20. {cfquant-0.2.70 → cfquant-0.2.71}/README.md +0 -0
  21. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/__init__.py +0 -0
  22. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/_editable_install.py +0 -0
  23. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/account_routing.py +0 -0
  24. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/algo.py +0 -0
  25. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/algo_broker.py +0 -0
  26. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/batch_orders.py +0 -0
  27. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/build_info.py +0 -0
  28. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/callback_dispatcher.py +0 -0
  29. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/cftrader.py +0 -0
  30. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/channels.py +0 -0
  31. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/cli.py +0 -0
  32. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/client.py +0 -0
  33. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/conditional_orders.py +0 -0
  34. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/config.py +0 -0
  35. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/diagnostic_context.py +0 -0
  36. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/diagnostics.py +0 -0
  37. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/flow_control.py +0 -0
  38. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/hub.py +0 -0
  39. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/level2.py +0 -0
  40. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/log_management.py +0 -0
  41. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/logging_i18n.py +0 -0
  42. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/management.py +0 -0
  43. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/monitor_notifications.py +0 -0
  44. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/monitor_performance.py +0 -0
  45. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/normal_bridge.py +0 -0
  46. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/notify.py +0 -0
  47. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/order_identity.py +0 -0
  48. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/order_meta.py +0 -0
  49. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/pipe_bridge.py +0 -0
  50. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/pipe_client.py +0 -0
  51. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/pipe_hub.py +0 -0
  52. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/pipe_transport.py +0 -0
  53. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/planning_broker.py +0 -0
  54. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/position_sizing.py +0 -0
  55. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/protocol.py +0 -0
  56. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_binding_defaults.py +0 -0
  57. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_binding_settings.py +0 -0
  58. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_bridge.py +0 -0
  59. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_component_agent.js +0 -0
  60. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_component_login.py +0 -0
  61. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_credentials.py +0 -0
  62. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_dependencies.py +0 -0
  63. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_inner.py +0 -0
  64. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_inner_catalog.py +0 -0
  65. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_inner_runtime.py +0 -0
  66. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_mac_agent.js +0 -0
  67. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_native_files.py +0 -0
  68. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_native_worker.py +0 -0
  69. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_pipe_runtime.py +0 -0
  70. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_strategy_agent.js +0 -0
  71. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_strategy_deploy.py +0 -0
  72. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_strategy_package.py +0 -0
  73. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_strategy_recovery.py +0 -0
  74. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_strategy_runtime.py +0 -0
  75. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_update.py +0 -0
  76. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_vm_agent.js +0 -0
  77. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_vm_discovery.py +0 -0
  78. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_vm_runtime.py +0 -0
  79. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_vm_settings.py +0 -0
  80. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_vm_worker.py +0 -0
  81. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/quote_history.py +0 -0
  82. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/quote_leases.py +0 -0
  83. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/quote_service.py +0 -0
  84. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/remote_adapter.py +0 -0
  85. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/remote_management.py +0 -0
  86. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/remote_supervisor.py +0 -0
  87. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/rpc_timing.py +0 -0
  88. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/runtime_report.py +0 -0
  89. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/shared_quotes.py +0 -0
  90. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/smart_orders.py +0 -0
  91. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/stock_connect.py +0 -0
  92. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/subscription_bridge.py +0 -0
  93. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/subscription_client.py +0 -0
  94. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/subscription_manager.py +0 -0
  95. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/subscription_store.py +0 -0
  96. {cfquant-0.2.70 → 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
  97. {cfquant-0.2.70 → 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
  98. {cfquant-0.2.70 → 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
  99. {cfquant-0.2.70 → 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
  100. {cfquant-0.2.70 → 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
  101. {cfquant-0.2.70 → 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
  102. {cfquant-0.2.70 → 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
  103. {cfquant-0.2.70 → 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
  104. {cfquant-0.2.70 → 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
  105. {cfquant-0.2.70 → 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
  106. {cfquant-0.2.70 → 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
  107. {cfquant-0.2.70 → 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
  108. {cfquant-0.2.70 → 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
  109. {cfquant-0.2.70 → 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
  110. {cfquant-0.2.70 → 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
  111. {cfquant-0.2.70 → 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
  112. {cfquant-0.2.70 → 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
  113. {cfquant-0.2.70 → 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
  114. {cfquant-0.2.70 → 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
  115. {cfquant-0.2.70 → 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
  116. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/29_Level2/350/241/214/346/203/205/346/265/213/350/257/225.py" +0 -0
  117. {cfquant-0.2.70 → 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
  118. {cfquant-0.2.70 → 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
  119. {cfquant-0.2.70 → 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
  120. {cfquant-0.2.70 → 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
  121. {cfquant-0.2.70 → 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
  122. {cfquant-0.2.70 → 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
  123. {cfquant-0.2.70 → 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
  124. {cfquant-0.2.70 → 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
  125. {cfquant-0.2.70 → 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
  126. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/6_/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
  127. {cfquant-0.2.70 → 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
  128. {cfquant-0.2.70 → 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
  129. {cfquant-0.2.70 → 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
  130. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/_helpers.py +0 -0
  131. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/qmt_vm_native_fixture.js +0 -0
  132. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/readme.md +0 -0
  133. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_account_query_timing.py +0 -0
  134. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_account_subscription_probe.py +0 -0
  135. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_activity_log_browser.py +0 -0
  136. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_advanced_parity.py +0 -0
  137. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_algo.py +0 -0
  138. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_algo_broker.py +0 -0
  139. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_api_tester_browser.py +0 -0
  140. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_api_tester_web.py +0 -0
  141. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_auto_login_app.py +0 -0
  142. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
  143. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_binding_live_refresh_browser.py +0 -0
  144. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_binding_python_runtime_browser.py +0 -0
  145. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_binding_runtime_browser.py +0 -0
  146. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_binding_vm_browser.py +0 -0
  147. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_binding_webhook_browser.py +0 -0
  148. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_callback_recovery.py +0 -0
  149. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_callback_requests.py +0 -0
  150. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_cftrader.py +0 -0
  151. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_cftrader_tutorial.py +0 -0
  152. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_cftrader_web.py +0 -0
  153. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_credit_compat.py +0 -0
  154. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_dataframe_encoding.py +0 -0
  155. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_diagnostic_context.py +0 -0
  156. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_history_download_memory.py +0 -0
  157. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_json_serialization.py +0 -0
  158. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_latency_flow_control.py +0 -0
  159. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_lite_account_routing.py +0 -0
  160. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_lite_deployment_paths.py +0 -0
  161. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_lite_parity.py +0 -0
  162. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_lite_qmt_dispatch.py +0 -0
  163. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_local_trading_calendar.py +0 -0
  164. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_log_management.py +0 -0
  165. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_management.py +0 -0
  166. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_management_batch.py +0 -0
  167. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_market_data_fallback.py +0 -0
  168. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_market_timing.py +0 -0
  169. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_monitor_notifications.py +0 -0
  170. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_monitor_performance.py +0 -0
  171. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_navigation_browser.py +0 -0
  172. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_notify.py +0 -0
  173. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_order_identity.py +0 -0
  174. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_order_time_callback.py +0 -0
  175. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_pipe_shutdown.py +0 -0
  176. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_project_updater.py +0 -0
  177. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_python_api_reference.py +0 -0
  178. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_auto_login_browser.py +0 -0
  179. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_auto_login_integration.py +0 -0
  180. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_binding_defaults.py +0 -0
  181. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_binding_settings.py +0 -0
  182. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_component_login.py +0 -0
  183. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_dependencies.py +0 -0
  184. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_fileless.py +0 -0
  185. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_inner.py +0 -0
  186. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_inner_reference.py +0 -0
  187. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_inner_web.py +0 -0
  188. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_native_files.py +0 -0
  189. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_native_reuse.py +0 -0
  190. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_native_worker.py +0 -0
  191. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_package_import.py +0 -0
  192. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_python_runtime.py +0 -0
  193. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_strategy_browser.py +0 -0
  194. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_strategy_capabilities.py +0 -0
  195. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_strategy_deploy.py +0 -0
  196. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_strategy_recovery.py +0 -0
  197. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_strategy_web.py +0 -0
  198. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_update.py +0 -0
  199. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_vm_discovery.py +0 -0
  200. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_vm_native.py +0 -0
  201. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_vm_runtime.py +0 -0
  202. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_vm_settings.py +0 -0
  203. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_quote_latency.py +0 -0
  204. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_quote_latency_encoding.py +0 -0
  205. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_quote_service.py +0 -0
  206. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_remote_management.py +0 -0
  207. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_remote_management_browser.py +0 -0
  208. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_remote_version_check.py +0 -0
  209. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
  210. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_shared_quotes.py +0 -0
  211. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_shared_quotes_generated.py +0 -0
  212. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_smart_orders.py +0 -0
  213. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_smart_orders_api.py +0 -0
  214. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_smart_orders_docs.py +0 -0
  215. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_split_order_identity.py +0 -0
  216. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_startup_recovery.py +0 -0
  217. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_stock_connect.py +0 -0
  218. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_stock_connect_browser.py +0 -0
  219. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_strategy_recovery_api.py +0 -0
  220. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_subscription_congestion.py +0 -0
  221. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_subscription_persistence.py +0 -0
  222. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_table_conversion.py +0 -0
  223. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_trade_strategy_callback.py +0 -0
  224. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_trader_subscription_recovery.py +0 -0
  225. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_trading_dates.py +0 -0
  226. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_trading_monitor.py +0 -0
  227. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_trading_monitor_browser.py +0 -0
  228. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_tutorial_reader.py +0 -0
  229. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_tx_auto_import.py +0 -0
  230. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_tx_shutdown.py +0 -0
  231. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_version_check.py +0 -0
  232. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_version_check_browser.py +0 -0
  233. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tools/THIRD_PARTY.md +0 -0
  234. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tools/upx/COPYING +0 -0
  235. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tools/upx/LICENSE +0 -0
  236. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tools/upx/NEWS +0 -0
  237. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tools/upx/README +0 -0
  238. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tools/upx/THANKS.txt +0 -0
  239. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tools/upx/upx-doc.html +0 -0
  240. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tools/upx/upx-doc.txt +0 -0
  241. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tools/upx/upx.1 +0 -0
  242. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tools/upx/upx.exe +0 -0
  243. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/trading_calendar.py +0 -0
  244. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/trading_monitor.py +0 -0
  245. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tx.py +0 -0
  246. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tx_trade_bridge.py +0 -0
  247. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/webhook.py +0 -0
  248. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/xtconstant.py +0 -0
  249. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/xtdata.py +0 -0
  250. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/xttrader.py +0 -0
  251. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/xttype.py +0 -0
  252. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant.egg-info/dependency_links.txt +0 -0
  253. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant.egg-info/entry_points.txt +0 -0
  254. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant.egg-info/requires.txt +0 -0
  255. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant.egg-info/top_level.txt +0 -0
  256. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant_pipe_hub.py +0 -0
  257. {cfquant-0.2.70 → cfquant-0.2.71}/cfquant_web_server.py +0 -0
  258. {cfquant-0.2.70 → cfquant-0.2.71}/qmt_scripts/CFQUANT.py +0 -0
  259. {cfquant-0.2.70 → cfquant-0.2.71}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +0 -0
  260. {cfquant-0.2.70 → cfquant-0.2.71}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
  261. {cfquant-0.2.70 → cfquant-0.2.71}/qmt_scripts/__init__.py +0 -0
  262. {cfquant-0.2.70 → 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.70 → 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.70 → 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.70 → 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.70 → 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.70 → cfquant-0.2.71}/requirements.txt +0 -0
  268. {cfquant-0.2.70 → cfquant-0.2.71}/setup.cfg +0 -0
  269. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/__init__.py +0 -0
  270. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/activity-log.js +0 -0
  271. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/api-tester.js +0 -0
  272. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/avatars/amber-pulse.svg +0 -0
  273. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/avatars/copper-grid.svg +0 -0
  274. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/avatars/market-blue.svg +0 -0
  275. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/avatars/rose-circuit.svg +0 -0
  276. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/avatars/signal-green.svg +0 -0
  277. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/avatars/slate-wave.svg +0 -0
  278. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/avatars/teal-orbit.svg +0 -0
  279. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/avatars/violet-node.svg +0 -0
  280. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/cfquant-architecture.html +0 -0
  281. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/cftrader-api-data.js +0 -0
  282. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/docs.js +0 -0
  283. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/favicon.svg +0 -0
  284. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
  285. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
  286. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
  287. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
  288. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
  289. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
  290. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
  291. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
  292. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
  293. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
  294. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
  295. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
  296. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
  297. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
  298. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
  299. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
  300. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
  301. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
  302. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/navigation.js +0 -0
  303. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/notify-api-data.js +0 -0
  304. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/python-api-data.js +0 -0
  305. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/python-api.js +0 -0
  306. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/qmt-inner-api-data.js +0 -0
  307. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/remote-management.js +0 -0
  308. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/smart-order-api-data.js +0 -0
  309. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/styles.css +0 -0
  310. {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/trading-monitor.js +0 -0
  311. {cfquant-0.2.70 → 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.70
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)
@@ -0,0 +1,181 @@
1
+ """Source routing, byte integrity and cancellation without contacting live QMT."""
2
+ from dataclasses import replace
3
+ import hashlib
4
+ import http.client
5
+ import io
6
+ import json
7
+ import threading
8
+ import urllib.error
9
+
10
+ import pytest
11
+
12
+ from cfquant import qmt_python_runtime as runtime
13
+
14
+ DATA = b'QMT test runtime archive' * 100
15
+
16
+
17
+ class Response(io.BytesIO):
18
+ def __init__(self, data=DATA, status=200, headers=None):
19
+ super().__init__(data)
20
+ self.status = status
21
+ self.headers = headers or {'Content-Length': str(len(data))}
22
+
23
+ def getcode(self):
24
+ return self.status
25
+
26
+
27
+ @pytest.fixture
28
+ def setup(tmp_path, monkeypatch):
29
+ manager = runtime.QmtPythonRuntime(tmp_path)
30
+ package = runtime.QmtPythonPackage('py312', '2.0.3.12', 'http://xuntou.test/runtime.zip',
31
+ 'runtime.zip', hashlib.md5(DATA).hexdigest(), True,
32
+ hashlib.sha256(DATA).hexdigest(), len(DATA))
33
+ snapshot = dict(ok=True, schema=1, busy=False, packages={'py312': package.as_dict()})
34
+ monkeypatch.setattr(manager, '_mirror_status', lambda: snapshot)
35
+ calls = []
36
+ def request(url, **kwargs):
37
+ calls.append((url, kwargs.get('headers', {})))
38
+ return Response()
39
+ monkeypatch.setattr(manager, '_request', request)
40
+ return manager, package, snapshot, calls
41
+
42
+
43
+ def test_mirror_first_and_verified_cache_needs_no_network(setup, monkeypatch):
44
+ manager, package, snapshot, calls = setup
45
+ assert manager.fetch_package() == package
46
+ assert manager.download(package=package).read_bytes() == DATA
47
+ assert calls[0][0].startswith(runtime._MIRROR_BASE)
48
+ assert calls[0][1][runtime._MIRROR_KEY_HEADER] == runtime._MIRROR_KEY
49
+ monkeypatch.setattr(manager, '_mirror_status', lambda: pytest.fail('cached file should work offline'))
50
+ manager.download(package=package)
51
+ assert len(calls) == 1
52
+
53
+
54
+ def test_busy_uses_upstream_without_mirror_auth_header(setup):
55
+ manager, package, snapshot, calls = setup
56
+ snapshot['busy'] = True
57
+ manager.download(package=package)
58
+ assert calls == [(package.url, {'Accept-Encoding': 'identity'})]
59
+
60
+
61
+ @pytest.mark.parametrize('busy', [True, False])
62
+ def test_upstream_failure_forces_website_even_when_busy(setup, monkeypatch, busy):
63
+ manager, package, snapshot, calls = setup
64
+ snapshot['busy'] = busy
65
+ def request(url, **kwargs):
66
+ calls.append(url)
67
+ if 'force=1' not in url:
68
+ raise urllib.error.URLError('offline')
69
+ return Response()
70
+ monkeypatch.setattr(manager, '_request', request)
71
+ assert manager.download(package=package).read_bytes() == DATA
72
+ assert calls[-1].endswith('&force=1')
73
+ assert calls[-2] == package.url
74
+ assert len(calls) == (2 if busy else 3)
75
+
76
+
77
+ def test_unavailable_status_still_attempts_direct_forced_download(setup, monkeypatch):
78
+ manager, package, snapshot, calls = setup
79
+ monkeypatch.setattr(manager, '_mirror_status', lambda: None)
80
+ def request(url, **kwargs):
81
+ calls.append(url)
82
+ if url == package.url:
83
+ raise TimeoutError('timeout')
84
+ return Response()
85
+ monkeypatch.setattr(manager, '_request', request)
86
+ manager.download(package=package)
87
+ assert len(calls) == 2 and calls[-1].endswith('&force=1')
88
+
89
+
90
+ def test_corrupt_download_is_discarded_before_fallback(setup, monkeypatch):
91
+ manager, package, snapshot, calls = setup
92
+ def request(url, **kwargs):
93
+ calls.append((url, kwargs['headers']))
94
+ return Response(b'x' * len(DATA)) if len(calls) == 1 else Response()
95
+ monkeypatch.setattr(manager, '_request', request)
96
+ assert manager.download(package=package).read_bytes() == DATA
97
+ assert calls[-1][0] == package.url and 'Range' not in calls[-1][1]
98
+
99
+
100
+ def test_truncated_response_resumes_at_other_source(setup, monkeypatch):
101
+ manager, package, snapshot, calls = setup
102
+ def request(url, **kwargs):
103
+ calls.append((url, kwargs['headers']))
104
+ if len(calls) == 1:
105
+ return Response(DATA[:100], headers={'Content-Length': str(len(DATA))})
106
+ assert kwargs['headers']['Range'] == 'bytes=100-'
107
+ return Response(DATA[100:], status=206,
108
+ headers={'Content-Range': 'bytes 100-%d/%d' % (len(DATA)-1, len(DATA))})
109
+ monkeypatch.setattr(manager, '_request', request)
110
+ assert manager.download(package=package).read_bytes() == DATA
111
+ assert len(calls) == 2
112
+
113
+
114
+ def test_incorrect_range_cannot_be_appended(setup, monkeypatch):
115
+ manager, package, snapshot, calls = setup
116
+ def request(url, **kwargs):
117
+ calls.append(url)
118
+ if len(calls) == 1:
119
+ return Response(DATA[:100], headers={'Content-Length': str(len(DATA))})
120
+ if len(calls) == 2:
121
+ return Response(DATA, status=206, headers={'Content-Range': 'bytes 0-%d/%d' % (len(DATA)-1, len(DATA))})
122
+ return Response() # A server may ignore Range and send a complete 200.
123
+ monkeypatch.setattr(manager, '_request', request)
124
+ assert manager.download(package=package).read_bytes() == DATA
125
+ assert len(calls) == 3
126
+
127
+
128
+ def test_sha256_failure_cannot_be_installed(setup, monkeypatch):
129
+ manager, package, snapshot, calls = setup
130
+ package = replace(package, sha256='0' * 64)
131
+ with pytest.raises(runtime.QmtPythonRuntimeError, match='SHA-256 mismatch'):
132
+ manager.download(package=package)
133
+ assert len(calls) == 3
134
+ assert not (manager.download_dir / package.filename).exists()
135
+
136
+
137
+ def test_cancel_does_not_fall_back(setup):
138
+ manager, package, snapshot, calls = setup
139
+ cancel = threading.Event()
140
+ def progress(event):
141
+ if event['bytes_done']:
142
+ cancel.set()
143
+ with pytest.raises(runtime.QmtPythonRuntimeError, match='cancelled'):
144
+ manager.download(package=package, cancel_event=cancel, progress_callback=progress)
145
+ assert len(calls) == 1
146
+
147
+
148
+ def test_local_write_failure_does_not_fall_back(setup, monkeypatch):
149
+ manager, package, snapshot, calls = setup
150
+ monkeypatch.setattr(runtime.os, 'fsync', lambda fd: (_ for _ in ()).throw(OSError('disk full')))
151
+ with pytest.raises(OSError, match='disk full'):
152
+ manager.download(package=package)
153
+ assert len(calls) == 1
154
+
155
+
156
+ def test_config_fallback_and_status_timeout_cache(tmp_path, monkeypatch):
157
+ manager = runtime.QmtPythonRuntime(tmp_path)
158
+ calls = []
159
+ def request(url, **kwargs):
160
+ calls.append((url, kwargs))
161
+ if url.startswith(runtime._MIRROR_BASE):
162
+ raise TimeoutError('offline')
163
+ return Response(b'[python]\nversion=2.0.3.12\nurl=http://xuntou.test/runtime.zip\nfilename=runtime.zip\nmd5=' + b'1'*32)
164
+ monkeypatch.setattr(manager, '_request', request)
165
+ assert manager.fetch_package().md5 == '1'*32
166
+ assert manager.fetch_package().md5 == '1'*32
167
+ assert len(calls) == 3
168
+ assert calls[0][1]['timeout'] == 5
169
+ assert 'headers' not in calls[-1][1]
170
+
171
+
172
+ def test_manifest_version_mismatch_never_uses_normal_mirror(setup):
173
+ manager, package, snapshot, calls = setup
174
+ snapshot['packages']['py312']['md5'] = '0'*32
175
+ manager.download(package=package)
176
+ assert calls[0][0] == package.url
177
+
178
+
179
+ def test_key_is_not_forwarded_on_redirect():
180
+ handler = runtime._NoMirrorRedirect()
181
+ assert handler.redirect_request(None, None, 302, '', {}, 'https://third-party.test') is None
@@ -1,5 +1,5 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """cfquant core version metadata."""
3
3
 
4
- __version__ = "0.2.70"
5
- WEB_VERSION = "web_20261112_01"
4
+ __version__ = "0.2.71"
5
+ WEB_VERSION = "web_20261112_02"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cfquant
3
- Version: 0.2.70
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
@@ -198,6 +198,7 @@ cfquant/tests/test_qmt_native_files.py
198
198
  cfquant/tests/test_qmt_native_reuse.py
199
199
  cfquant/tests/test_qmt_native_worker.py
200
200
  cfquant/tests/test_qmt_package_import.py
201
+ cfquant/tests/test_qmt_python_mirror.py
201
202
  cfquant/tests/test_qmt_python_runtime.py
202
203
  cfquant/tests/test_qmt_strategy_browser.py
203
204
  cfquant/tests/test_qmt_strategy_capabilities.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cfquant"
7
- version = "0.2.70"
7
+ version = "0.2.71"
8
8
  description = "QMT local bridge and xtquant-compatible Python client."
9
9
  license = {text = "MIT"}
10
10
  readme = "README.md"
@@ -753,7 +753,7 @@ def _resolve_batch_order_ids(bridge, account, pending, before_ids):
753
753
  time.sleep(min(0.05, remaining))
754
754
  # END GENERATED CFTRADER BATCH
755
755
 
756
- CORE_VERSION = "0.2.70"
756
+ CORE_VERSION = "0.2.71"
757
757
 
758
758
  # BEGIN GENERATED DIAGNOSTIC CONTEXT
759
759
  """Version context for cfquant diagnostics without changing application logging."""
@@ -767,7 +767,7 @@ def _resolve_batch_order_ids(bridge, account, pending, before_ids):
767
767
  time.sleep(min(0.05, remaining))
768
768
  # END GENERATED CFTRADER BATCH
769
769
 
770
- CORE_VERSION = "0.2.70"
770
+ CORE_VERSION = "0.2.71"
771
771
 
772
772
  # BEGIN GENERATED DIAGNOSTIC CONTEXT
773
773
  """Version context for cfquant diagnostics without changing application logging."""
@@ -767,7 +767,7 @@ def _resolve_batch_order_ids(bridge, account, pending, before_ids):
767
767
  time.sleep(min(0.05, remaining))
768
768
  # END GENERATED CFTRADER BATCH
769
769
 
770
- CORE_VERSION = "0.2.70"
770
+ CORE_VERSION = "0.2.71"
771
771
 
772
772
  # BEGIN GENERATED DIAGNOSTIC CONTEXT
773
773
  """Version context for cfquant diagnostics without changing application logging."""
@@ -1,4 +1,4 @@
1
- const FRONTEND_VERSION = 'web_20261112_01';
1
+ const FRONTEND_VERSION = 'web_20261112_02';
2
2
 
3
3
  const state = {
4
4
  accountId: '',
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>cfquant 控制台</title>
7
7
  <link rel="icon" type="image/svg+xml" href="/favicon.svg">
8
- <link rel="stylesheet" href="/styles.css?v=web_20261112_01">
8
+ <link rel="stylesheet" href="/styles.css?v=web_20261112_02">
9
9
  </head>
10
10
  <body>
11
11
  <div id="webAuthOverlay" class="auth-overlay hidden" aria-live="polite">
@@ -3585,7 +3585,7 @@ else:
3585
3585
  <script src="/trading-monitor.js?v=20261111_runtime_window_01"></script>
3586
3586
  <script src="/navigation.js?v=web_20261108_nav_03"></script>
3587
3587
  <script src="/activity-log.js?v=web_20261108_logs_01"></script>
3588
- <script src="/app.js?v=web_20261112_01"></script>
3588
+ <script src="/app.js?v=web_20261112_02"></script>
3589
3589
  <script src="/remote-management.js?v=20261103"></script>
3590
3590
  </body>
3591
3591
  </html>
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes