cfquant 0.2.47__tar.gz → 0.2.49__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 (174) hide show
  1. {cfquant-0.2.47 → cfquant-0.2.49}/PKG-INFO +1 -1
  2. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/qmt_strategy_deploy.py +95 -44
  3. {cfquant-0.2.47 → cfquant-0.2.49}/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 +4 -4
  4. cfquant-0.2.49/cfquant/tests/test_lite_deployment_paths.py +169 -0
  5. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_qmt_strategy_deploy.py +71 -0
  6. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_qmt_strategy_web.py +4 -3
  7. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/version.py +2 -2
  8. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant.egg-info/PKG-INFO +1 -1
  9. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant.egg-info/SOURCES.txt +1 -0
  10. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant_web_server.py +115 -79
  11. {cfquant-0.2.47 → cfquant-0.2.49}/pyproject.toml +1 -1
  12. {cfquant-0.2.47 → cfquant-0.2.49}/qmt_scripts/CFQUANT_LITE.py +1 -1
  13. {cfquant-0.2.47 → cfquant-0.2.49}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_LITE_SH.py +1 -1
  14. {cfquant-0.2.47 → cfquant-0.2.49}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_LITE_SZ.py +1 -1
  15. {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/app.js +1 -1
  16. {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/index.html +2 -2
  17. {cfquant-0.2.47 → cfquant-0.2.49}/LICENSE +0 -0
  18. {cfquant-0.2.47 → cfquant-0.2.49}/LTtx/__init__.py +0 -0
  19. {cfquant-0.2.47 → cfquant-0.2.49}/LTtx/tx/Config.txt +0 -0
  20. {cfquant-0.2.47 → cfquant-0.2.49}/LTtx/tx/LTtx_server.py +0 -0
  21. {cfquant-0.2.47 → cfquant-0.2.49}/LTtx/tx/__init__.py +0 -0
  22. {cfquant-0.2.47 → cfquant-0.2.49}/LTtx/tx/tx.py +0 -0
  23. {cfquant-0.2.47 → cfquant-0.2.49}/MANIFEST.in +0 -0
  24. {cfquant-0.2.47 → cfquant-0.2.49}/README.md +0 -0
  25. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/__init__.py +0 -0
  26. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/_editable_install.py +0 -0
  27. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/account_routing.py +0 -0
  28. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/batch_orders.py +0 -0
  29. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/build_info.py +0 -0
  30. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/callback_dispatcher.py +0 -0
  31. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/cftrader.py +0 -0
  32. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/channels.py +0 -0
  33. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/cli.py +0 -0
  34. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/client.py +0 -0
  35. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/config.py +0 -0
  36. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/hub.py +0 -0
  37. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/level2.py +0 -0
  38. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/log_management.py +0 -0
  39. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/logging_i18n.py +0 -0
  40. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/management.py +0 -0
  41. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/normal_bridge.py +0 -0
  42. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/order_identity.py +0 -0
  43. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/order_meta.py +0 -0
  44. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/pipe_bridge.py +0 -0
  45. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/pipe_client.py +0 -0
  46. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/pipe_hub.py +0 -0
  47. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/pipe_transport.py +0 -0
  48. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/protocol.py +0 -0
  49. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/qmt_bridge.py +0 -0
  50. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/qmt_strategy_package.py +0 -0
  51. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/qmt_strategy_runtime.py +0 -0
  52. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/runtime_report.py +0 -0
  53. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/stock_connect.py +0 -0
  54. {cfquant-0.2.47 → cfquant-0.2.49}/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
  55. {cfquant-0.2.47 → cfquant-0.2.49}/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
  56. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/12_/350/256/242/345/215/225/345/244/207/346/263/250/346/265/213/350/257/225.py" +0 -0
  57. {cfquant-0.2.47 → cfquant-0.2.49}/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
  58. {cfquant-0.2.47 → cfquant-0.2.49}/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
  59. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/16_/345/220/257/345/212/250/345/256/211/350/243/205/346/265/213/350/257/225.py" +0 -0
  60. {cfquant-0.2.47 → cfquant-0.2.49}/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
  61. {cfquant-0.2.47 → cfquant-0.2.49}/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
  62. {cfquant-0.2.47 → cfquant-0.2.49}/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
  63. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/1_/350/241/214/346/203/205/346/216/245/346/224/266/346/265/213/350/257/225.py" +0 -0
  64. {cfquant-0.2.47 → cfquant-0.2.49}/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
  65. {cfquant-0.2.47 → cfquant-0.2.49}/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
  66. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/22_/350/277/236/346/216/245/350/257/212/346/226/255/346/265/213/350/257/225.py" +0 -0
  67. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/23_/347/244/272/344/276/213/351/205/215/347/275/256/346/265/213/350/257/225.py" +0 -0
  68. {cfquant-0.2.47 → cfquant-0.2.49}/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
  69. {cfquant-0.2.47 → cfquant-0.2.49}/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
  70. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/26_/345/205/250/346/216/250/350/256/242/351/230/205/346/265/213/350/257/225.py" +0 -0
  71. {cfquant-0.2.47 → cfquant-0.2.49}/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
  72. {cfquant-0.2.47 → cfquant-0.2.49}/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
  73. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/29_Level2/350/241/214/346/203/205/346/265/213/350/257/225.py" +0 -0
  74. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/2_/346/225/260/346/215/256/350/216/267/345/217/226/346/265/213/350/257/225.py" +0 -0
  75. {cfquant-0.2.47 → cfquant-0.2.49}/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
  76. {cfquant-0.2.47 → cfquant-0.2.49}/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
  77. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/32_/345/274/202/346/255/245/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  78. {cfquant-0.2.47 → cfquant-0.2.49}/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
  79. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/3_/346/225/260/346/215/256/344/270/213/350/275/275/346/265/213/350/257/225.py" +0 -0
  80. {cfquant-0.2.47 → cfquant-0.2.49}/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
  81. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/5_/347/234/237/345/256/236/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  82. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/6_/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
  83. {cfquant-0.2.47 → cfquant-0.2.49}/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
  84. {cfquant-0.2.47 → cfquant-0.2.49}/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
  85. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/9_/345/217/257/346/222/244/350/256/242/345/215/225/346/265/213/350/257/225.py" +0 -0
  86. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/_helpers.py +0 -0
  87. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/log/cftrader/346/250/241/346/213/237/350/264/246/345/217/267/346/211/271/351/207/217/344/270/213/345/215/225/346/265/213/350/257/225_20260911_024617.md" +0 -0
  88. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/log/cftrader/346/250/241/346/213/237/350/264/246/345/217/267/346/211/271/351/207/217/344/270/213/345/215/225/346/265/213/350/257/225_20260921_175732.md" +0 -0
  89. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/readme.md +0 -0
  90. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_api_tester_browser.py +0 -0
  91. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_api_tester_web.py +0 -0
  92. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
  93. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_callback_requests.py +0 -0
  94. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_cftrader.py +0 -0
  95. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_cftrader_tutorial.py +0 -0
  96. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_cftrader_web.py +0 -0
  97. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_credit_compat.py +0 -0
  98. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_json_serialization.py +0 -0
  99. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_lite_account_routing.py +0 -0
  100. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_local_trading_calendar.py +0 -0
  101. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_log_management.py +0 -0
  102. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_management.py +0 -0
  103. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_management_batch.py +0 -0
  104. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_market_data_fallback.py +0 -0
  105. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_order_identity.py +0 -0
  106. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_order_time_callback.py +0 -0
  107. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_project_updater.py +0 -0
  108. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_python_api_reference.py +0 -0
  109. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_qmt_strategy_browser.py +0 -0
  110. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
  111. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_startup_recovery.py +0 -0
  112. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_stock_connect.py +0 -0
  113. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_stock_connect_browser.py +0 -0
  114. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_trading_dates.py +0 -0
  115. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_tutorial_reader.py +0 -0
  116. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tests/test_tx_auto_import.py +0 -0
  117. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/trading_calendar.py +0 -0
  118. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tx.py +0 -0
  119. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/tx_trade_bridge.py +0 -0
  120. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/xtconstant.py +0 -0
  121. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/xtdata.py +0 -0
  122. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/xttrader.py +0 -0
  123. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant/xttype.py +0 -0
  124. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant.egg-info/dependency_links.txt +0 -0
  125. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant.egg-info/entry_points.txt +0 -0
  126. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant.egg-info/requires.txt +0 -0
  127. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant.egg-info/top_level.txt +0 -0
  128. {cfquant-0.2.47 → cfquant-0.2.49}/cfquant_pipe_hub.py +0 -0
  129. {cfquant-0.2.47 → cfquant-0.2.49}/qmt_scripts/CFQUANT.py +0 -0
  130. {cfquant-0.2.47 → cfquant-0.2.49}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +0 -0
  131. {cfquant-0.2.47 → cfquant-0.2.49}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
  132. {cfquant-0.2.47 → cfquant-0.2.49}/qmt_scripts/__init__.py +0 -0
  133. {cfquant-0.2.47 → cfquant-0.2.49}/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
  134. {cfquant-0.2.47 → cfquant-0.2.49}/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
  135. {cfquant-0.2.47 → cfquant-0.2.49}/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
  136. {cfquant-0.2.47 → cfquant-0.2.49}/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
  137. {cfquant-0.2.47 → cfquant-0.2.49}/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
  138. {cfquant-0.2.47 → cfquant-0.2.49}/requirements.txt +0 -0
  139. {cfquant-0.2.47 → cfquant-0.2.49}/setup.cfg +0 -0
  140. {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/__init__.py +0 -0
  141. {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/api-tester.js +0 -0
  142. {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/avatars/amber-pulse.svg +0 -0
  143. {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/avatars/copper-grid.svg +0 -0
  144. {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/avatars/market-blue.svg +0 -0
  145. {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/avatars/rose-circuit.svg +0 -0
  146. {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/avatars/signal-green.svg +0 -0
  147. {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/avatars/slate-wave.svg +0 -0
  148. {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/avatars/teal-orbit.svg +0 -0
  149. {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/avatars/violet-node.svg +0 -0
  150. {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/cftrader-api-data.js +0 -0
  151. {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/docs.js +0 -0
  152. {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/favicon.svg +0 -0
  153. {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
  154. {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
  155. {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
  156. {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
  157. {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
  158. {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
  159. {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
  160. {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
  161. {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
  162. {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
  163. {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
  164. {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
  165. {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
  166. {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
  167. {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
  168. {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
  169. {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
  170. {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
  171. {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/python-api-data.js +0 -0
  172. {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/python-api.js +0 -0
  173. {cfquant-0.2.47 → cfquant-0.2.49}/web_dashboard/styles.css +0 -0
  174. {cfquant-0.2.47 → cfquant-0.2.49}/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.47
3
+ Version: 0.2.49
4
4
  Summary: QMT local bridge and xtquant-compatible Python client.
5
5
  License: MIT
6
6
  Requires-Python: >=3.8
@@ -23,7 +23,10 @@ MODE_ALIASES = {
23
23
  "ctypes": "ctypes", "pipe": "ctypes", "named_pipe": "ctypes",
24
24
  "lite": "lite", "extreme": "lite", "extreme_lite": "lite",
25
25
  "lite_extreme": "lite", "ultimate": "lite",
26
+ "lite_extreme_pipe": "lite", "extreme_pipe": "lite", "cfquant_lite": "lite",
27
+ "named-pipe": "ctypes", "universal": "ctypes", "universal_ctypes": "ctypes",
26
28
  "lttx": "lttx", "socket": "lttx", "normal": "lttx",
29
+ "default": "lttx",
27
30
  }
28
31
  LEGACY_NAMES = {Path(name).stem for name in SCRIPTS.values()} | {"CFQUANT_TRADE_LOWLAT"}
29
32
  LEGACY_NAMES |= {name + "_" + market for name in tuple(LEGACY_NAMES) for market in ("SH", "SZ")}
@@ -68,9 +71,42 @@ def normalize_strategy_mode(value):
68
71
  return MODE_ALIASES[mode]
69
72
  except KeyError:
70
73
  raise ValueError("unknown QMT strategy mode: %s" % mode)
71
-
72
-
73
- def qmt_root(directory, validate=False):
74
+
75
+
76
+ def entry_script_for_mode(mode, role="normal", market=""):
77
+ """One mapping for raw copies and the packages QMT actually imports."""
78
+ mode = normalize_strategy_mode(mode)
79
+ market = str(market or "").strip().upper()
80
+ if market and market not in ("SH", "SZ"):
81
+ raise ValueError("unknown QMT entry market: %s" % market)
82
+ filename = SCRIPTS[mode]
83
+ if mode == "lttx" and (role != "normal" or market):
84
+ filename = "CFQUANT_TRADE_LOWLAT.py"
85
+ if market:
86
+ return "同账号独立市场/%s_%s.py" % (Path(filename).stem, market)
87
+ return filename
88
+
89
+
90
+ def read_entry_source(path):
91
+ """Reject a generic entry accidentally saved under a Lite filename."""
92
+ path = Path(path)
93
+ with tokenize.open(str(path)) as stream:
94
+ source = stream.read()
95
+ if path.stem.startswith("CFQUANT_LITE"):
96
+ tree = ast.parse(source)
97
+ constants = {target.id for node in tree.body if isinstance(node, ast.Assign)
98
+ for target in node.targets if isinstance(target, ast.Name)}
99
+ imports_core = any(
100
+ (isinstance(node, ast.ImportFrom) and (node.module or "").split(".")[0] == "cfquant")
101
+ or (isinstance(node, ast.Import) and any(alias.name.split(".")[0] == "cfquant"
102
+ for alias in node.names))
103
+ for node in ast.walk(tree))
104
+ if "LITE_ENTRY_VERSION" not in constants or imports_core:
105
+ raise ValueError("极致模式入口内容不匹配,拒绝部署通用模式代码: %s" % path)
106
+ return source
107
+
108
+
109
+ def qmt_root(directory, validate=False):
74
110
  path = Path(str(directory)).expanduser().resolve()
75
111
  if path.name.lower() in ("bin.x64", "bin", "python"):
76
112
  path = path.parent
@@ -283,9 +319,8 @@ def _resolve_account_binding(root, document, account_id, account_type, supplied=
283
319
  return None
284
320
 
285
321
 
286
- def managed_source(source_path, identity_path, identity, descriptor):
287
- with tokenize.open(str(source_path)) as stream:
288
- source = stream.read()
322
+ def managed_source(source_path, identity_path, identity, descriptor):
323
+ source = read_entry_source(source_path)
289
324
  lines = source.splitlines(keepends=True)
290
325
  tree = ast.parse(source)
291
326
  replacements = []
@@ -308,13 +343,23 @@ def managed_source(source_path, identity_path, identity, descriptor):
308
343
  for start, end, text in sorted(replacements, reverse=True):
309
344
  lines[start:end] = [text]
310
345
  lines[0] = "# coding: utf-8\n"
311
- source = "".join(lines)
312
- ast.parse(source, feature_version=(3, 6))
313
- helper = Path(__file__).with_name("qmt_strategy_runtime.py").read_text(encoding="utf-8")
314
- generated = ("# coding: utf-8\n" + helper + "\n_cq_descriptor = " + ascii(descriptor)
315
- + "\n_cq_guard = _CqStrategyLease(_cq_descriptor).acquire()\n"
316
- + "_cq_guard.namespace = globals()\ntry:\n"
317
- + " exec(compile(%s, %s, 'exec'), globals())\n" % (ascii(source), ascii(str(source_path)))
346
+ source = "".join(lines)
347
+ ast.parse(source, feature_version=(3, 6))
348
+ helper = Path(__file__).with_name("qmt_strategy_runtime.py").read_text(encoding="utf-8")
349
+ source_lines = source.splitlines(keepends=True)
350
+ # Adjacent literals compile to the same source string, while keeping every
351
+ # entry line visible in QMT's editor. A single ascii(source) hid thousands
352
+ # of Lite lines behind the same short lease wrapper as the other modes.
353
+ embedded = "\n".join(" " + ascii(line) for line in source_lines)
354
+ header = ("# coding: utf-8\n# cfquant managed strategy\n"
355
+ + "# Mode: " + ascii(descriptor["mode"]) + "\n"
356
+ + "# Entry: " + ascii(Path(source_path).name) + "\n"
357
+ + "# Embedded entry: %d lines, preserved line by line below.\n" % len(source_lines)
358
+ + "# The lease wrapper is shared by all modes.\n\n")
359
+ generated = (header + helper + "\n_cq_descriptor = " + ascii(descriptor)
360
+ + "\n_cq_guard = _CqStrategyLease(_cq_descriptor).acquire()\n"
361
+ + "_cq_guard.namespace = globals()\ntry:\n"
362
+ + " exec(compile((\n%s\n ), %s, 'exec'), globals())\n" % (embedded, ascii(str(source_path)))
318
363
  + " for _cq_name in ('_cf_bridge', '_normal_bridge', '_trade_bridge'):\n"
319
364
  + " if globals().get(_cq_name) is not None:\n"
320
365
  + " globals()[_cq_name].account_type = %s\n" % ascii(identity["account_type"])
@@ -373,13 +418,17 @@ class QmtStrategyManager:
373
418
  job.pop("deploy_retry_count", None)
374
419
  job.pop("deploy_retry_after", None)
375
420
 
376
- def _clear_generation_transition(self, job):
377
- changed = False
378
- for key in ("control_generation", "accepted_generations"):
379
- if key in job:
380
- job.pop(key, None)
381
- changed = True
382
- return changed
421
+ def _clear_generation_transition(self, job):
422
+ changed = False
423
+ for key in ("control_generation", "accepted_generations"):
424
+ if key in job:
425
+ job.pop(key, None)
426
+ changed = True
427
+ for role in job.get("roles", []):
428
+ if "reimport_from_generation" in role:
429
+ role.pop("reimport_from_generation", None)
430
+ changed = True
431
+ return changed
383
432
 
384
433
  def _prepare_generation_transition(self, job, previous=None):
385
434
  """Keep old same-name containers valid until the replacement starts."""
@@ -432,19 +481,23 @@ class QmtStrategyManager:
432
481
  )
433
482
  job["control_generation"] = legacy
434
483
  job["accepted_generations"] = accepted
435
- if previous is None:
436
- for role in job.get("roles", []):
437
- if _runtime_generation(role) == legacy:
438
- updates = {
439
- "reimport_required": True,
440
- "imported": False,
441
- "queued": False,
442
- "model_prepared": False,
443
- }
444
- for key, value in updates.items():
445
- if role.get(key) != value:
446
- role[key] = value
447
- changed = True
484
+ for role in job.get("roles", []):
485
+ if (_runtime_generation(role) == legacy
486
+ and role.get("reimport_from_generation") != legacy):
487
+ # The old report survives QMT shutdown and import. Handle it
488
+ # once per replacement job, including across Web restarts;
489
+ # resetting every poll destroys the prepared/imported model.
490
+ updates = {
491
+ "reimport_from_generation": legacy,
492
+ "reimport_required": True,
493
+ "imported": False,
494
+ "queued": False,
495
+ "model_prepared": False,
496
+ }
497
+ for key, value in updates.items():
498
+ if role.get(key) != value:
499
+ role[key] = value
500
+ changed = True
448
501
  if changed and job.get("state") == "error":
449
502
  self._state(job, "waiting_exit")
450
503
  return changed
@@ -463,12 +516,13 @@ class QmtStrategyManager:
463
516
  result.update(error=self.load_error, message=self.load_error)
464
517
  return result
465
518
 
466
- def configure(self, row, identities, force=False):
519
+ def configure(self, row, identities, force=False, scripts_dir=None):
467
520
  if self.load_error:
468
521
  raise ValueError(self.load_error)
469
522
  settings = normalize_strategy_settings(row.get("qmt_strategy"))
470
523
  mode = normalize_strategy_mode(row.get("mode"))
471
- enabled = row.get("enabled", True) and settings["enabled"]
524
+ enabled = row.get("enabled", True) and settings["enabled"]
525
+ scripts_dir = Path(scripts_dir) if scripts_dir is not None else self.scripts_dir
472
526
  with self.lock:
473
527
  wanted = set()
474
528
  grouped = {}
@@ -486,19 +540,16 @@ class QmtStrategyManager:
486
540
  "account_key": row["account_key"]}])
487
541
  market = info.get("market") or identity.get("market") or ""
488
542
  role = market or info.get("qmt_role") or "normal"
489
- filename = SCRIPTS[mode]
490
- if mode == "lttx" and role != "normal":
491
- filename = "CFQUANT_TRADE_LOWLAT.py"
492
- source_path = self.scripts_dir / filename
493
- if market:
494
- source_path = self.scripts_dir / "同账号独立市场" / (Path(filename).stem + "_" + market + ".py")
495
- if not source_path.is_file():
496
- raise ValueError("QMT 内置脚本不存在: %s" % source_path)
543
+ filename = entry_script_for_mode(mode, role, market)
544
+ source_path = scripts_dir / filename
545
+ if not source_path.is_file():
546
+ raise ValueError("QMT 内置脚本不存在: %s" % source_path)
547
+ read_entry_source(source_path)
497
548
  for key in ("updated_at", "updated_at_text"):
498
549
  identity.pop(key, None)
499
550
  group_key = _digest([os.path.normcase(str(root)), row["account_id"]])[:24]
500
551
  grouped.setdefault(group_key, {"root": str(root), "roles": []})["roles"].append({
501
- "role": role, "source": str(source_path), "identity": identity,
552
+ "role": role, "source": str(source_path), "entry_script": filename, "identity": identity,
502
553
  "source_sha256": hashlib.sha256(source_path.read_bytes()).hexdigest(),
503
554
  "account_key": settings["account_keys"].get(role, "")})
504
555
  if not grouped:
@@ -372,7 +372,7 @@ def test_auto_deploy_local_core_skips_when_target_matches(tmp_path):
372
372
  def test_local_entry_sync_overwrites_bound_qmt_python_script(tmp_path):
373
373
  source = tmp_path / "source"
374
374
  (source / "qmt_scripts").mkdir(parents=True)
375
- (source / "qmt_scripts" / "CFQUANT_LITE.py").write_bytes(b"new-entry\n")
375
+ (source / "qmt_scripts" / "CFQUANT_LITE.py").write_bytes(b'LITE_ENTRY_VERSION = "test"\n')
376
376
  qmt_root = tmp_path / "QMT"
377
377
  (qmt_root / "bin.x64").mkdir(parents=True)
378
378
  (qmt_root / "python").mkdir()
@@ -389,7 +389,7 @@ def test_local_entry_sync_overwrites_bound_qmt_python_script(tmp_path):
389
389
  )
390
390
 
391
391
  assert result["updated"] is True
392
- assert entry.read_bytes() == b"new-entry\n"
392
+ assert entry.read_bytes() == b'LITE_ENTRY_VERSION = "test"\n'
393
393
  assert result["entry_file"] == str(entry)
394
394
  assert result["backup"]
395
395
  assert (qmt_root / "bin.x64" / ".cfquant_updates" / "entry_backups").is_dir()
@@ -402,7 +402,7 @@ def test_auto_deploy_project_update_copies_bound_mode_entry(tmp_path, monkeypatc
402
402
  for name in ("__init__.py", "client.py", "protocol.py"):
403
403
  (source_core / name).write_text("# source\n", encoding="utf-8")
404
404
  (source / "qmt_scripts").mkdir()
405
- (source / "qmt_scripts" / "CFQUANT_LITE.py").write_bytes(b"latest-lite\n")
405
+ (source / "qmt_scripts" / "CFQUANT_LITE.py").write_bytes(b'LITE_ENTRY_VERSION = "latest"\n')
406
406
 
407
407
  qmt_root = tmp_path / "QMT"
408
408
  target_core = qmt_root / "bin.x64" / "cfquant"
@@ -423,7 +423,7 @@ def test_auto_deploy_project_update_copies_bound_mode_entry(tmp_path, monkeypatc
423
423
 
424
424
  assert result["summary"]["ok"] is True
425
425
  assert result["summary"]["entry_updated_count"] == 1
426
- assert (qmt_root / "python" / "CFQUANT_LITE.py").read_bytes() == b"latest-lite\n"
426
+ assert (qmt_root / "python" / "CFQUANT_LITE.py").read_bytes() == b'LITE_ENTRY_VERSION = "latest"\n'
427
427
  assert result["results"][0]["entry_script"] == "CFQUANT_LITE.py"
428
428
 
429
429
 
@@ -0,0 +1,169 @@
1
+ """Verify the bytes copied and imported for Lite across binding/update paths."""
2
+
3
+ import ast
4
+ import json
5
+ from pathlib import Path
6
+ import shutil
7
+
8
+ import pytest
9
+
10
+ from cfquant.qmt_strategy_deploy import QmtStrategyManager
11
+ from cfquant.qmt_strategy_package import _cipher
12
+ from cfquant.tests.test_qmt_strategy_deploy import deployment, finish_import, models, SCRIPTS
13
+
14
+
15
+ def embedded_source(package):
16
+ content = json.loads(_cipher().decrypt(package.read_bytes()).decode("utf-8", "surrogateescape"))["content"]
17
+ return next(node.args[0].value for node in ast.walk(ast.parse(content))
18
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Name)
19
+ and node.func.id == "compile" and isinstance(node.args[0], ast.Constant))
20
+
21
+
22
+ def assert_lite_package(manager, root):
23
+ job = next(job for job in manager.jobs.values() if job.get("enabled"))
24
+ assert job["mode"] == "lite"
25
+ for role in job["roles"]:
26
+ source = embedded_source(root / "formulas" / (role["name"] + ".rzrk"))
27
+ assert "LITE_ENTRY_VERSION =" in source
28
+ assert "def account_route_client_ids(" in source
29
+ assert not any(isinstance(node, ast.ImportFrom) and (node.module or "").startswith("cfquant")
30
+ for node in ast.walk(ast.parse(source)))
31
+ assert Path(role["source"]).name.startswith("CFQUANT_LITE")
32
+ return job
33
+
34
+
35
+ @pytest.fixture
36
+ def web_deployment(deployment, tmp_path, monkeypatch):
37
+ import cfquant_web_server as web
38
+ manager, row, infos, running, root = deployment
39
+ config = web.WebRuntimeConfig(str(tmp_path / "web.json"), str(tmp_path / "web.db"))
40
+ config._data["account_configs"] = {row["account_key"]: row}
41
+ monkeypatch.setattr(web, "WEB_CONFIG", config)
42
+ monkeypatch.setattr(web, "BASE_DIR", str(SCRIPTS.parent))
43
+ monkeypatch.setattr(web, "QMT_STRATEGIES", manager)
44
+ updater = web.CfquantUpdater(config)
45
+ monkeypatch.setattr(updater, "install_local_core_to_qmt_dir", lambda *args, **kwargs: {"skipped": True})
46
+ monkeypatch.setattr(web, "UPDATER", updater)
47
+ monkeypatch.setattr(web, "write_qmt_bridge_identity", lambda row: dict(infos[0]))
48
+ monkeypatch.setattr(web, "write_qmt_market_bridge_identities", lambda row: [])
49
+ monkeypatch.setattr(web, "sync_qmt_bridge_identities", lambda: [])
50
+ monkeypatch.setattr(web, "qmt_process_preflight", lambda **kwargs: None)
51
+ monkeypatch.setattr(web, "qmt_auto_login_apply_for_account", lambda *args, **kwargs: {})
52
+ monkeypatch.setattr(web, "ensure_account_runtime", lambda mode: {})
53
+ monkeypatch.setattr(web.ACCOUNT_CACHE, "prime_configured_accounts", lambda: None)
54
+ monkeypatch.setattr(web.STATUS_MONITOR, "wake", lambda: None)
55
+ monkeypatch.setattr(web.CALLBACKS, "refresh_channels", lambda channels: None)
56
+ return web, manager, row, infos, root
57
+
58
+
59
+ @pytest.mark.parametrize("action", ["bind", "account_update", "project_update", "startup"])
60
+ def test_selected_lite_replaces_imported_generic_code(action, web_deployment, monkeypatch):
61
+ web, manager, row, infos, root = web_deployment
62
+ manager.configure(row, infos)
63
+ finish_import(manager, root)
64
+ manager.process_once()
65
+ generic_name = next(iter(manager.jobs.values()))["roles"][0]["name"]
66
+ # Simulate an older installation with a Lite binding and a generic import.
67
+ row["mode"] = "lite"
68
+ if action == "bind":
69
+ result = web.save_account_runtime_config({
70
+ key: row[key] for key in ("account_id", "account_type", "bridge_id", "qmt_dir", "mode")})
71
+ assert result["account"]["mode"] == "lite"
72
+ assert not result["qmt_strategy_deploy"].get("error")
73
+ elif action == "account_update":
74
+ monkeypatch.setattr(web, "account_config_for_request", lambda body: row)
75
+ result = web.update_account_qmt_core({"account_key": row["account_key"]})
76
+ assert not result["qmt_strategy_deploy"].get("error")
77
+ elif action == "project_update":
78
+ result = web.auto_deploy_qmt_core_for_all_accounts(str(SCRIPTS.parent), sync_entry_scripts=True)
79
+ assert result["summary"]["ok"]
80
+ else:
81
+ # Persisted jobs must be repaired on first start after an old updater.
82
+ manager = QmtStrategyManager(manager.path, SCRIPTS, lambda root: False)
83
+ monkeypatch.setattr(web, "QMT_STRATEGIES", manager)
84
+ result = web.refresh_bound_qmt_strategies()
85
+ assert not result[0].get("error")
86
+ assert_lite_package(manager, root)
87
+ assert models(root)[generic_name].getAttribute("startupAutorun") == "0"
88
+ if action != "startup":
89
+ assert (root / "python" / "CFQUANT_LITE.py").read_bytes() == (SCRIPTS / "CFQUANT_LITE.py").read_bytes()
90
+
91
+
92
+ def test_edit_without_mode_preserves_lite_binding_and_payload(web_deployment):
93
+ web, manager, row, _, root = web_deployment
94
+ row["mode"] = "lite"
95
+ result = web.save_account_runtime_config({
96
+ key: row[key] for key in ("account_id", "account_type", "bridge_id", "qmt_dir")})
97
+ assert result["account"]["mode"] == "lite"
98
+ assert_lite_package(manager, root)
99
+
100
+
101
+ def test_edit_advanced_binding_to_lite_retires_both_advanced_models(web_deployment):
102
+ web, manager, row, infos, root = web_deployment
103
+ trade_root = root.with_name("QMT_trade")
104
+ shutil.copytree(root, trade_root)
105
+ row.update(mode="lttx", qmt_trade_dir=str(trade_root / "bin.x64"))
106
+ row["qmt_strategy"]["autorun"] = True
107
+ trade_info = dict(infos[0], qmt_role="trade", core_dir=str(trade_root / "bin.x64"),
108
+ path=str(trade_root / "bin.x64" / "cfquant_bridge_config.json"))
109
+ manager.configure(row, infos + [trade_info])
110
+ advanced_name = "CFQ_%s_LTTX" % row["account_id"]
111
+ assert models(root)[advanced_name].getAttribute("startupAutorun") == "1"
112
+ assert models(trade_root)[advanced_name].getAttribute("startupAutorun") == "1"
113
+
114
+ body = {key: row[key] for key in ("account_id", "account_type", "bridge_id", "qmt_dir")}
115
+ result = web.save_account_runtime_config(dict(body, mode="lite", qmt_trade_dir=""))
116
+
117
+ assert result["account"]["mode"] == "lite"
118
+ assert result["account"]["qmt_trade_dir"] == ""
119
+ assert not result["qmt_strategy_deploy"].get("error")
120
+ job = assert_lite_package(manager, root)
121
+ assert job["roles"][0]["name"] == "CFQ_%s_LITE" % row["account_id"]
122
+ assert models(root)[job["roles"][0]["name"]].getAttribute("startupAutorun") == "1"
123
+ assert models(root)[advanced_name].getAttribute("startupAutorun") == "0"
124
+ assert models(trade_root)[advanced_name].getAttribute("startupAutorun") == "0"
125
+ trade_job = next(job for job in manager.jobs.values() if job["root"] == str(trade_root))
126
+ assert trade_job["enabled"] is False
127
+ assert trade_job["state"] == "disabled"
128
+
129
+
130
+ @pytest.mark.parametrize("single_account", [False, True])
131
+ def test_shared_qmt_directory_does_not_drop_lite_market_entries(single_account, web_deployment):
132
+ web, _, row, _, root = web_deployment
133
+ row.update(mode="lite", market_routing_enabled=True,
134
+ qmt_strategy={"enabled": False}, market_bridges={
135
+ market: {"qmt_dir": str(root), "bridge_id": "bridge_" + market}
136
+ for market in ("SH", "SZ")})
137
+ if single_account:
138
+ result = web.auto_deploy_qmt_core_for_account(row)
139
+ else:
140
+ # The earlier binding must not win solely because it shares a directory.
141
+ generic = dict(row, account_key="generic", account_id="222", mode="ctypes", market_routing_enabled=False)
142
+ web.WEB_CONFIG._data["account_configs"] = {"generic": generic, row["account_key"]: row}
143
+ result = web.auto_deploy_qmt_core_for_all_accounts(str(SCRIPTS.parent), sync_entry_scripts=True)
144
+ assert result["summary"]["ok"]
145
+ for name, directory in (("CFQUANT_LITE.py", SCRIPTS),
146
+ ("CFQUANT_LITE_SH.py", SCRIPTS / "同账号独立市场"),
147
+ ("CFQUANT_LITE_SZ.py", SCRIPTS / "同账号独立市场")):
148
+ assert (root / "python" / name).read_bytes() == (directory / name).read_bytes()
149
+
150
+
151
+ def test_update_uses_new_source_and_rejects_mislabeled_generic_entry(web_deployment, tmp_path):
152
+ web, manager, row, _, root = web_deployment
153
+ row["mode"] = "lite"
154
+ source = tmp_path / "updated_project"
155
+ (source / "qmt_scripts").mkdir(parents=True)
156
+ entry = source / "qmt_scripts" / "CFQUANT_LITE.py"
157
+ entry.write_bytes((SCRIPTS / entry.name).read_bytes() + b'\nRELEASE_TEST_MARKER = "new-source"\n')
158
+ result = web.auto_deploy_qmt_core_for_all_accounts(str(source), sync_entry_scripts=True)
159
+ assert result["summary"]["ok"]
160
+ job = assert_lite_package(manager, root)
161
+ assert 'RELEASE_TEST_MARKER = "new-source"' in embedded_source(
162
+ root / "formulas" / (job["roles"][0]["name"] + ".rzrk"))
163
+ previous = (root / "python" / entry.name).read_bytes()
164
+ shutil.copyfile(SCRIPTS / "CFQUANT_CTYPE_ALL_LOWLAT.py", entry)
165
+ result = web.auto_deploy_qmt_core_for_all_accounts(str(source), sync_entry_scripts=True)
166
+ assert not result["summary"]["ok"]
167
+ assert result["summary"]["strategy_error_count"] == 1
168
+ assert "通用模式代码" in result["summary"]["message"]
169
+ assert (root / "python" / entry.name).read_bytes() == previous
@@ -15,6 +15,7 @@ from cfquant.qmt_strategy_deploy import (
15
15
  QmtStrategyManager, _account_binding, _atomic_write, _read_document,
16
16
  _write_json, _resolve_account_binding, account_qmt_roots, managed_source, normalize_strategy_settings,
17
17
  normalize_strategy_mode,
18
+ entry_script_for_mode, read_entry_source,
18
19
  _strategy_slot_name,
19
20
  )
20
21
  from cfquant.qmt_strategy_package import _cipher, build_package
@@ -31,6 +32,14 @@ def test_strategy_mode_aliases_are_canonical(value, expected):
31
32
  assert normalize_strategy_mode(value) == expected
32
33
 
33
34
 
35
+ def test_lite_entry_mapping_rejects_generic_source(tmp_path):
36
+ assert entry_script_for_mode("extreme") == "CFQUANT_LITE.py"
37
+ source = tmp_path / "CFQUANT_LITE.py"
38
+ source.write_text("from cfquant import __version__\n", encoding="utf-8")
39
+ with pytest.raises(ValueError, match="通用模式代码"):
40
+ read_entry_source(source)
41
+
42
+
34
43
  def test_strategy_name_contains_mode_and_shortens_long_account():
35
44
  name = _strategy_slot_name("ACCOUNT-12345678901234567890", "lite")
36
45
  assert name == "CFQ_67890_LITE"
@@ -455,6 +464,8 @@ def test_all_managed_entry_variants_compile_without_executing(mode, deployment):
455
464
  assert document["detail"]["realName"] == role["name"] + ".py"
456
465
  assert document["formulaCatalog"][0].encode("utf-8", "surrogateescape").hex() == "ced2b5c4b2dfc2d4"
457
466
  ast.parse(document["content"], feature_version=(3, 6))
467
+ assert "# Mode: %r\n" % mode in document["content"]
468
+ assert "# Entry: %r\n" % Path(role["source"]).name in document["content"]
458
469
  if mode == "lite":
459
470
  embedded = [node.args[0].value for node in ast.walk(ast.parse(document["content"]))
460
471
  if isinstance(node, ast.Call) and isinstance(node.func, ast.Name)
@@ -468,6 +479,8 @@ def test_all_managed_entry_variants_compile_without_executing(mode, deployment):
468
479
  if isinstance(node, (ast.FunctionDef, ast.ClassDef))}
469
480
  assert definitions(actual) == definitions(original)
470
481
  assert len(embedded[0].splitlines()) > 7000
482
+ assert len(document["content"].splitlines()) > 7000
483
+ assert max(map(len, document["content"].splitlines())) < 2000
471
484
  descriptor = {"mode": mode, "generation": "test", "role": "SH", "control_path": "test", "lease_dir": "test"}
472
485
  variants = list((SCRIPTS / "同账号独立市场").glob("*.py")) + [SCRIPTS / "CFQUANT_TRADE_LOWLAT.py"]
473
486
  for path in variants:
@@ -513,6 +526,64 @@ def test_process_once_detects_stale_runtime_generation_and_queues_reimport(deplo
513
526
  assert (root / "formulas" / (role["name"] + ".rzrk")).is_file()
514
527
 
515
528
 
529
+ @pytest.mark.parametrize("report_state", ["running", "stopped"])
530
+ @pytest.mark.parametrize("previous_mode", ["lite", "lttx"])
531
+ def test_old_runtime_report_does_not_reset_prepared_replacement(deployment, report_state, previous_mode):
532
+ manager, row, infos, running, root = deployment
533
+ row["mode"] = "lite"
534
+ manager.configure(row, infos)
535
+ finish_import(manager, root)
536
+ manager.process_once()
537
+ first = copy.deepcopy(next(iter(manager.jobs.values())))
538
+ _write_json(first["roles"][0]["runtime_status_path"], {
539
+ "state": report_state, "generation": first["generation"], "updated_at": time.time() - 120,
540
+ })
541
+ if previous_mode != "lite":
542
+ row["mode"] = previous_mode
543
+ manager.configure(row, infos)
544
+ finish_import(manager, root)
545
+ manager.process_once()
546
+
547
+ row["mode"] = "lite"
548
+ manager.configure(row, infos, force=True)
549
+ job = next(iter(manager.jobs.values()))
550
+ role = job["roles"][0]
551
+ queue = root / "formulas" / (role["name"] + ".rzrk")
552
+ assert queue.is_file()
553
+ assert role["model_prepared"] is True
554
+
555
+ # The old heartbeat remains on disk until QMT has imported and started
556
+ # the new strategy. Repeated polling or a Web restart must not undo it.
557
+ manager = QmtStrategyManager(manager.path, SCRIPTS, lambda root: running[0])
558
+ job = next(iter(manager.jobs.values()))
559
+ role = job["roles"][0]
560
+ running[0] = True
561
+ manager.process_once()
562
+ manager.process_once()
563
+ assert role["reimport_required"] is False
564
+ assert role["model_prepared"] is True
565
+ assert role["queued"] is True
566
+ assert job["state"] == "waiting_manual_start"
567
+
568
+ finish_import(manager, root)
569
+ container = root / "python" / (role["name"] + ".py")
570
+ running[0] = False
571
+ manager.process_once()
572
+ manager.process_once()
573
+ assert container.is_file()
574
+ assert not queue.exists()
575
+ assert role["imported"] is True
576
+ assert job["state"] == "configured"
577
+
578
+ _write_json(role["runtime_status_path"], {
579
+ "state": "running", "generation": job["generation"], "updated_at": time.time(),
580
+ })
581
+ running[0] = True
582
+ manager.process_once()
583
+ assert job["state"] == "running"
584
+ assert not job.get("control_generation")
585
+
586
+
516
587
  @pytest.mark.parametrize("encoding", ["utf-8", "gbk", "gb18030"])
517
588
  def test_managed_package_executes_after_qmt_encoding_with_chinese_paths(tmp_path, encoding):
518
589
  directory = tmp_path / "\u534e\u6cf0\u8bc1\u5238 QMT \u6a21\u62df"
@@ -282,8 +282,9 @@ def test_save_and_delete_call_deployment_manager(web_config, monkeypatch):
282
282
  class Manager:
283
283
  def reconcile(self, accounts):
284
284
  calls.append(("reconcile", set(accounts)))
285
- def configure(self, row, identities, force=False):
285
+ def configure(self, row, identities, force=False, scripts_dir=None):
286
286
  assert force is True
287
+ assert scripts_dir == os.path.join(web.BASE_DIR, "qmt_scripts")
287
288
  calls.append(("configure", row["qmt_strategy"], identities))
288
289
  return {"enabled": True, "targets": [{"state": "waiting_exit"}]}
289
290
  monkeypatch.setattr(web, "QMT_STRATEGIES", Manager())
@@ -303,7 +304,7 @@ def test_save_and_delete_call_deployment_manager(web_config, monkeypatch):
303
304
  assert calls[-1] == ("reconcile", set())
304
305
 
305
306
 
306
- def test_save_can_start_qmt_auto_login_and_persist_restart_times(web_config, monkeypatch):
307
+ def test_save_can_start_qmt_auto_login_and_persist_restart_times(web_config, monkeypatch, tmp_path):
307
308
  web, config = web_config
308
309
  calls = []
309
310
  monkeypatch.setattr(web, "auto_deploy_qmt_core_for_account", lambda *args, **kwargs: {})
@@ -324,7 +325,7 @@ def test_save_can_start_qmt_auto_login_and_persist_restart_times(web_config, mon
324
325
  data = web.save_account_runtime_config({
325
326
  "account_id": "8885060548",
326
327
  "display_name": "国金证券",
327
- "qmt_dir": r"D:\国金证券QMT交易端\bin.x64",
328
+ "qmt_dir": str(tmp_path / "QMT" / "bin.x64"),
328
329
  "qmt_auto_login": {"enabled": True, "restart_times": ["6:30", "06:30", "18:05"]},
329
330
  })
330
331
  assert data["qmt_auto_login"]["pid"] == 1234
@@ -1,5 +1,5 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """cfquant core version metadata."""
3
3
 
4
- __version__ = "0.2.47"
5
- WEB_VERSION = "web_20261011_01"
4
+ __version__ = "0.2.49"
5
+ WEB_VERSION = "web_20261012_01"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cfquant
3
- Version: 0.2.47
3
+ Version: 0.2.49
4
4
  Summary: QMT local bridge and xtquant-compatible Python client.
5
5
  License: MIT
6
6
  Requires-Python: >=3.8
@@ -99,6 +99,7 @@ cfquant/tests/test_cftrader_web.py
99
99
  cfquant/tests/test_credit_compat.py
100
100
  cfquant/tests/test_json_serialization.py
101
101
  cfquant/tests/test_lite_account_routing.py
102
+ cfquant/tests/test_lite_deployment_paths.py
102
103
  cfquant/tests/test_local_trading_calendar.py
103
104
  cfquant/tests/test_log_management.py
104
105
  cfquant/tests/test_management.py