cfquant 0.2.50__tar.gz → 0.2.51__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 (179) hide show
  1. {cfquant-0.2.50 → cfquant-0.2.51}/PKG-INFO +1 -1
  2. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/build_info.py +2 -0
  3. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/normal_bridge.py +8 -1
  4. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/order_identity.py +17 -0
  5. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/pipe_bridge.py +12 -2
  6. cfquant-0.2.51/cfquant/qmt_pipe_runtime.py +166 -0
  7. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/qmt_strategy_deploy.py +144 -29
  8. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/qmt_strategy_runtime.py +20 -11
  9. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_lite_deployment_paths.py +18 -9
  10. cfquant-0.2.51/cfquant/tests/test_qmt_fileless.py +271 -0
  11. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_qmt_strategy_deploy.py +150 -16
  12. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_trade_strategy_callback.py +68 -0
  13. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tx_trade_bridge.py +26 -13
  14. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/version.py +2 -2
  15. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/xttype.py +2 -1
  16. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant.egg-info/PKG-INFO +1 -1
  17. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant.egg-info/SOURCES.txt +2 -0
  18. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant_web_server.py +23 -9
  19. {cfquant-0.2.50 → cfquant-0.2.51}/pyproject.toml +1 -1
  20. {cfquant-0.2.50 → cfquant-0.2.51}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +248 -379
  21. {cfquant-0.2.50 → cfquant-0.2.51}/qmt_scripts/CFQUANT_LITE.py +264 -552
  22. {cfquant-0.2.50 → cfquant-0.2.51}/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 +196 -367
  23. {cfquant-0.2.50 → cfquant-0.2.51}/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 +196 -367
  24. {cfquant-0.2.50 → cfquant-0.2.51}/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 +264 -604
  25. {cfquant-0.2.50 → cfquant-0.2.51}/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 +264 -604
  26. {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/app.js +1 -1
  27. {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/index.html +2 -2
  28. {cfquant-0.2.50 → cfquant-0.2.51}/LICENSE +0 -0
  29. {cfquant-0.2.50 → cfquant-0.2.51}/LTtx/__init__.py +0 -0
  30. {cfquant-0.2.50 → cfquant-0.2.51}/LTtx/tx/Config.txt +0 -0
  31. {cfquant-0.2.50 → cfquant-0.2.51}/LTtx/tx/LTtx_server.py +0 -0
  32. {cfquant-0.2.50 → cfquant-0.2.51}/LTtx/tx/__init__.py +0 -0
  33. {cfquant-0.2.50 → cfquant-0.2.51}/LTtx/tx/tx.py +0 -0
  34. {cfquant-0.2.50 → cfquant-0.2.51}/MANIFEST.in +0 -0
  35. {cfquant-0.2.50 → cfquant-0.2.51}/README.md +0 -0
  36. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/__init__.py +0 -0
  37. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/_editable_install.py +0 -0
  38. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/account_routing.py +0 -0
  39. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/batch_orders.py +0 -0
  40. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/callback_dispatcher.py +0 -0
  41. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/cftrader.py +0 -0
  42. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/channels.py +0 -0
  43. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/cli.py +0 -0
  44. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/client.py +0 -0
  45. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/config.py +0 -0
  46. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/hub.py +0 -0
  47. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/level2.py +0 -0
  48. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/log_management.py +0 -0
  49. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/logging_i18n.py +0 -0
  50. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/management.py +0 -0
  51. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/order_meta.py +0 -0
  52. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/pipe_client.py +0 -0
  53. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/pipe_hub.py +0 -0
  54. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/pipe_transport.py +0 -0
  55. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/protocol.py +0 -0
  56. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/qmt_bridge.py +0 -0
  57. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/qmt_strategy_package.py +0 -0
  58. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/runtime_report.py +0 -0
  59. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/stock_connect.py +0 -0
  60. {cfquant-0.2.50 → cfquant-0.2.51}/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
  61. {cfquant-0.2.50 → cfquant-0.2.51}/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
  62. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/12_/350/256/242/345/215/225/345/244/207/346/263/250/346/265/213/350/257/225.py" +0 -0
  63. {cfquant-0.2.50 → cfquant-0.2.51}/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
  64. {cfquant-0.2.50 → cfquant-0.2.51}/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
  65. {cfquant-0.2.50 → cfquant-0.2.51}/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
  66. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/16_/345/220/257/345/212/250/345/256/211/350/243/205/346/265/213/350/257/225.py" +0 -0
  67. {cfquant-0.2.50 → cfquant-0.2.51}/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
  68. {cfquant-0.2.50 → cfquant-0.2.51}/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
  69. {cfquant-0.2.50 → cfquant-0.2.51}/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
  70. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/1_/350/241/214/346/203/205/346/216/245/346/224/266/346/265/213/350/257/225.py" +0 -0
  71. {cfquant-0.2.50 → cfquant-0.2.51}/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
  72. {cfquant-0.2.50 → cfquant-0.2.51}/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
  73. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/22_/350/277/236/346/216/245/350/257/212/346/226/255/346/265/213/350/257/225.py" +0 -0
  74. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/23_/347/244/272/344/276/213/351/205/215/347/275/256/346/265/213/350/257/225.py" +0 -0
  75. {cfquant-0.2.50 → cfquant-0.2.51}/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
  76. {cfquant-0.2.50 → cfquant-0.2.51}/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
  77. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/26_/345/205/250/346/216/250/350/256/242/351/230/205/346/265/213/350/257/225.py" +0 -0
  78. {cfquant-0.2.50 → cfquant-0.2.51}/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
  79. {cfquant-0.2.50 → cfquant-0.2.51}/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
  80. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/29_Level2/350/241/214/346/203/205/346/265/213/350/257/225.py" +0 -0
  81. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/2_/346/225/260/346/215/256/350/216/267/345/217/226/346/265/213/350/257/225.py" +0 -0
  82. {cfquant-0.2.50 → cfquant-0.2.51}/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
  83. {cfquant-0.2.50 → cfquant-0.2.51}/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
  84. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/32_/345/274/202/346/255/245/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  85. {cfquant-0.2.50 → cfquant-0.2.51}/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
  86. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/3_/346/225/260/346/215/256/344/270/213/350/275/275/346/265/213/350/257/225.py" +0 -0
  87. {cfquant-0.2.50 → cfquant-0.2.51}/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
  88. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/5_/347/234/237/345/256/236/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  89. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/6_/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
  90. {cfquant-0.2.50 → cfquant-0.2.51}/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
  91. {cfquant-0.2.50 → cfquant-0.2.51}/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
  92. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/9_/345/217/257/346/222/244/350/256/242/345/215/225/346/265/213/350/257/225.py" +0 -0
  93. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/_helpers.py +0 -0
  94. {cfquant-0.2.50 → cfquant-0.2.51}/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
  95. {cfquant-0.2.50 → cfquant-0.2.51}/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
  96. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/readme.md +0 -0
  97. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_advanced_parity.py +0 -0
  98. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_api_tester_browser.py +0 -0
  99. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_api_tester_web.py +0 -0
  100. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
  101. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_callback_requests.py +0 -0
  102. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_cftrader.py +0 -0
  103. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_cftrader_tutorial.py +0 -0
  104. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_cftrader_web.py +0 -0
  105. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_credit_compat.py +0 -0
  106. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_json_serialization.py +0 -0
  107. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_lite_account_routing.py +0 -0
  108. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_lite_parity.py +0 -0
  109. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_local_trading_calendar.py +0 -0
  110. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_log_management.py +0 -0
  111. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_management.py +0 -0
  112. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_management_batch.py +0 -0
  113. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_market_data_fallback.py +0 -0
  114. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_order_identity.py +0 -0
  115. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_order_time_callback.py +0 -0
  116. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_project_updater.py +0 -0
  117. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_python_api_reference.py +0 -0
  118. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_qmt_strategy_browser.py +0 -0
  119. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_qmt_strategy_web.py +0 -0
  120. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
  121. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_startup_recovery.py +0 -0
  122. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_stock_connect.py +0 -0
  123. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_stock_connect_browser.py +0 -0
  124. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_trading_dates.py +0 -0
  125. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_tutorial_reader.py +0 -0
  126. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tests/test_tx_auto_import.py +0 -0
  127. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/trading_calendar.py +0 -0
  128. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/tx.py +0 -0
  129. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/xtconstant.py +0 -0
  130. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/xtdata.py +0 -0
  131. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant/xttrader.py +0 -0
  132. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant.egg-info/dependency_links.txt +0 -0
  133. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant.egg-info/entry_points.txt +0 -0
  134. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant.egg-info/requires.txt +0 -0
  135. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant.egg-info/top_level.txt +0 -0
  136. {cfquant-0.2.50 → cfquant-0.2.51}/cfquant_pipe_hub.py +0 -0
  137. {cfquant-0.2.50 → cfquant-0.2.51}/qmt_scripts/CFQUANT.py +0 -0
  138. {cfquant-0.2.50 → cfquant-0.2.51}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
  139. {cfquant-0.2.50 → cfquant-0.2.51}/qmt_scripts/__init__.py +0 -0
  140. {cfquant-0.2.50 → cfquant-0.2.51}/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
  141. {cfquant-0.2.50 → cfquant-0.2.51}/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
  142. {cfquant-0.2.50 → cfquant-0.2.51}/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
  143. {cfquant-0.2.50 → cfquant-0.2.51}/requirements.txt +0 -0
  144. {cfquant-0.2.50 → cfquant-0.2.51}/setup.cfg +0 -0
  145. {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/__init__.py +0 -0
  146. {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/api-tester.js +0 -0
  147. {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/avatars/amber-pulse.svg +0 -0
  148. {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/avatars/copper-grid.svg +0 -0
  149. {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/avatars/market-blue.svg +0 -0
  150. {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/avatars/rose-circuit.svg +0 -0
  151. {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/avatars/signal-green.svg +0 -0
  152. {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/avatars/slate-wave.svg +0 -0
  153. {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/avatars/teal-orbit.svg +0 -0
  154. {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/avatars/violet-node.svg +0 -0
  155. {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/cftrader-api-data.js +0 -0
  156. {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/docs.js +0 -0
  157. {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/favicon.svg +0 -0
  158. {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
  159. {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
  160. {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
  161. {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
  162. {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
  163. {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
  164. {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
  165. {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
  166. {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
  167. {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
  168. {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
  169. {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
  170. {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
  171. {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
  172. {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
  173. {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
  174. {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
  175. {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
  176. {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/python-api-data.js +0 -0
  177. {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/python-api.js +0 -0
  178. {cfquant-0.2.50 → cfquant-0.2.51}/web_dashboard/styles.css +0 -0
  179. {cfquant-0.2.50 → cfquant-0.2.51}/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.50
3
+ Version: 0.2.51
4
4
  Summary: QMT local bridge and xtquant-compatible Python client.
5
5
  License: MIT
6
6
  Requires-Python: >=3.8
@@ -29,6 +29,8 @@ def get_git_commit(source_root=None):
29
29
  override = _valid_commit(os.environ.get("CFQUANT_GIT_COMMIT"))
30
30
  if override:
31
31
  return override
32
+ if os.environ.get("CFQUANT_QMT_NO_FILE_IO") == "1":
33
+ return ""
32
34
  root = Path(source_root or Path(__file__).resolve().parent.parent).resolve()
33
35
  try:
34
36
  result = subprocess.run(["git", "-C", str(root), "rev-parse", "HEAD"], stdout=subprocess.PIPE,
@@ -10,7 +10,7 @@ import threading
10
10
  import time
11
11
 
12
12
  from . import order_meta
13
- from .order_identity import prepare_order_remark, order_remark_key, restore_order_remark
13
+ from .order_identity import prepare_order_remark, order_remark_key, restore_order_remark, restore_strategy_name
14
14
  from .level2 import L2_THOUSAND_SUBSCRIPTIONS, quote_callback_data, quote_plain, require_l2_callable, thousand_price
15
15
  from .protocol import loads_message, pack_event, pack_response
16
16
  from .tx_trade_bridge import TxTradeBridge, relay_sync_order_callback
@@ -191,6 +191,12 @@ class NormalQmtBridge(TxTradeBridge):
191
191
  if action == "cfquant.status":
192
192
  self._send_response(msg, self._status())
193
193
  return
194
+ if action == "cfquant.strategy.control":
195
+ try:
196
+ self._send_response(msg, self._dispatch(action, msg.get("params") or {}, msg))
197
+ except Exception as error:
198
+ self._send_error(msg, error)
199
+ return
194
200
  if self._try_enqueue_coalesced_request(msg):
195
201
  return
196
202
  try:
@@ -894,6 +900,7 @@ class NormalQmtBridge(TxTradeBridge):
894
900
  relay_sync_order_callback(self.context, data)
895
901
  self._handle_async_order_callback(data)
896
902
  self._remember_order_callback_fields(data)
903
+ restore_strategy_name(data)
897
904
  payload = {
898
905
  "type": "event",
899
906
  "event": event_name,
@@ -12,6 +12,23 @@ from collections import OrderedDict
12
12
 
13
13
 
14
14
  _ORDER_REMARK_SUFFIX = re.compile(r"__cfq_([0-9a-f]{32})$")
15
+ _STRATEGY_NAME_SUFFIX = re.compile(r"(?:&&&_cfq_|^cfq_)[0-9a-f]{12}\Z")
16
+
17
+
18
+ def restore_strategy_name(data):
19
+ """Remove our request token only at the public callback/object boundary.
20
+
21
+ Keep the full strategy until error-context matching has finished. Match
22
+ the generated suffix precisely so user underscores and delimiters survive.
23
+ """
24
+ if isinstance(data, dict):
25
+ for name in ("strategy_name", "m_strStrategyName", "mstrStrategyName", "strategyName"):
26
+ value = data.get(name)
27
+ if isinstance(value, str):
28
+ match = _STRATEGY_NAME_SUFFIX.search(value)
29
+ if match:
30
+ data[name] = value[:match.start()]
31
+ return data
15
32
 
16
33
 
17
34
  def original_order_remark(value):
@@ -8,7 +8,9 @@ from .pipe_transport import DEFAULT_PIPE_NAME, PipeTxClient
8
8
  from .tx_trade_bridge import TxTradeBridge
9
9
 
10
10
 
11
- class PipeNormalQmtBridge(NormalQmtBridge):
11
+ class PipeNormalQmtBridge(NormalQmtBridge):
12
+ file_io_enabled = False
13
+
12
14
  def __init__(
13
15
  self,
14
16
  context,
@@ -48,6 +50,9 @@ class PipeNormalQmtBridge(NormalQmtBridge):
48
50
  self.connect_timeout_ms = int(connect_timeout_ms)
49
51
 
50
52
  def start(self):
53
+ runtime = self.globals_dict.get("_managed_runtime")
54
+ if runtime is not None:
55
+ runtime.attach_bridge(self)
51
56
  if self.running:
52
57
  return self
53
58
  self.running = True
@@ -103,7 +108,9 @@ class PipeNormalQmtBridge(NormalQmtBridge):
103
108
  return data
104
109
 
105
110
 
106
- class PipeTradeBridge(TxTradeBridge):
111
+ class PipeTradeBridge(TxTradeBridge):
112
+ file_io_enabled = False
113
+
107
114
  def __init__(
108
115
  self,
109
116
  context,
@@ -131,6 +138,9 @@ class PipeTradeBridge(TxTradeBridge):
131
138
  self.connect_timeout_ms = int(connect_timeout_ms)
132
139
 
133
140
  def start(self):
141
+ runtime = self.globals_dict.get("_managed_runtime")
142
+ if runtime is not None:
143
+ runtime.attach_bridge(self)
134
144
  if self.running:
135
145
  return self
136
146
  self.running = True
@@ -0,0 +1,166 @@
1
+ """In-memory managed strategy control over the existing Pipe connection (QMT 3.6+)."""
2
+
3
+ import functools as _cq_functools
4
+ import os as _cq_os
5
+ import sys as _cq_sys
6
+ import threading as _cq_threading
7
+ import time as _cq_time
8
+
9
+
10
+ class ManagedPipeRuntime(object):
11
+ def __init__(self, descriptor=None):
12
+ self.descriptor = dict(descriptor or {})
13
+ self.active = False
14
+ self.approved = False
15
+ self.revoked = False
16
+ self.namespace = {}
17
+ self.original_stop = None
18
+ self.failure = ""
19
+ self.calls = 0
20
+ self.bridges_closed = False
21
+ self.attached_bridges = []
22
+ self.mutex = _cq_threading.RLock()
23
+
24
+ def acquire(self):
25
+ if not self.descriptor:
26
+ self.active = True
27
+ return self
28
+ # sys survives strategy namespace reloads inside one QMT process.
29
+ # Across processes the Pipe hub arbitrates ownership of request channels.
30
+ registry = _cq_sys.__dict__.setdefault("_cfquant_pipe_strategies", {})
31
+ lock = _cq_sys.__dict__.setdefault("_cfquant_pipe_strategies_lock", _cq_threading.RLock())
32
+ with lock:
33
+ key = (self.descriptor["runtime_key"], self.descriptor["role"])
34
+ incompatible = any(scope[0] == key[0] and (
35
+ owner.descriptor.get("mode") != self.descriptor["mode"]
36
+ or owner.descriptor.get("generation") != self.descriptor["generation"])
37
+ for scope, owner in registry.items())
38
+ if key in registry or incompatible:
39
+ raise RuntimeError("Another cfquant strategy still owns this QMT account role.")
40
+ registry[key] = self
41
+ self.active = True
42
+ return self
43
+
44
+ def status(self):
45
+ bridges = [self.namespace.get(name) for name in ("_normal_bridge", "_trade_bridge")
46
+ if self.namespace.get(name) is not None]
47
+ ready = bool(bridges) and all(getattr(bridge, "running", False)
48
+ and getattr(bridge, "context", None) is not None for bridge in bridges)
49
+ state = "error" if self.failure else ("stopped" if not self.active or self.revoked
50
+ else ("running" if ready and self.approved else "loaded"))
51
+ return dict(self.descriptor, state=state, error=self.failure,
52
+ pid=_cq_os.getpid(), updated_at=_cq_time.time())
53
+
54
+ def control(self, desired):
55
+ if desired.get("runtime_key") != self.descriptor.get("runtime_key"):
56
+ raise ValueError("Managed strategy account scope mismatch")
57
+ accepted = desired.get("accepted_generations") or []
58
+ if not isinstance(accepted, (tuple, list)):
59
+ accepted = []
60
+ allowed = (desired.get("enabled") is True
61
+ and desired.get("mode") == self.descriptor["mode"]
62
+ and self.descriptor["generation"] in [desired.get("generation")] + accepted)
63
+ with self.mutex:
64
+ self.approved = allowed and self.active and not self.revoked
65
+ if not allowed and not self.revoked:
66
+ self.revoked = True
67
+ # Block requests immediately; let the control response leave before closing Pipe.
68
+ worker = _cq_threading.Timer(0.1, self.close)
69
+ worker.daemon = True
70
+ worker.start()
71
+ return self.status()
72
+
73
+ def dispatch(self, function):
74
+ dispatch_request = self.wrap(function, reject=True)
75
+ @_cq_functools.wraps(function)
76
+ def guarded(action, params, msg):
77
+ if action == "cfquant.strategy.control":
78
+ return self.control(params or {})
79
+ if action == "cfquant.status":
80
+ return function(action, params, msg)
81
+ if not self.approved:
82
+ raise RuntimeError("Managed strategy is awaiting account mode confirmation from cfquant Web.")
83
+ return dispatch_request(action, params, msg)
84
+ return guarded
85
+
86
+ def wrap(self, function, reject=False):
87
+ @_cq_functools.wraps(function)
88
+ def guarded(*args, **kwargs):
89
+ with self.mutex:
90
+ if not self.active or self.revoked:
91
+ if reject:
92
+ raise RuntimeError("The account's cfquant strategy mode has changed.")
93
+ return None
94
+ self.calls += 1
95
+ try:
96
+ return function(*args, **kwargs)
97
+ except Exception as error:
98
+ if function.__name__ == "init":
99
+ self.failure = str(error)
100
+ self.close()
101
+ raise
102
+ finally:
103
+ with self.mutex:
104
+ self.calls -= 1
105
+ self.release_if_idle()
106
+ return guarded
107
+
108
+ def bind(self, namespace):
109
+ if not self.descriptor:
110
+ return
111
+ self.namespace = namespace
112
+ self.original_stop = namespace.get("stop")
113
+ for name, value in list(namespace.items()):
114
+ if (name in ("init", "after_init", "handlebar", "cfquant_normal_timer", "cfquant_ctype_trade_timer")
115
+ or name.endswith("_callback")) and callable(value):
116
+ namespace[name] = self.wrap(value)
117
+ for name in ("_normal_bridge", "_trade_bridge"):
118
+ bridge = namespace.get(name)
119
+ if bridge is not None:
120
+ self.attach_bridge(bridge)
121
+ namespace["stop"] = self.close
122
+
123
+ def attach_bridge(self, bridge):
124
+ if bridge not in self.attached_bridges:
125
+ self.attached_bridges.append(bridge)
126
+ if not self.descriptor or getattr(bridge, "managed_runtime", None) is self:
127
+ return
128
+ bridge.managed_runtime = self
129
+ bridge._dispatch = self.dispatch(bridge._dispatch)
130
+ for method in ("start", "poll", "pump"):
131
+ function = getattr(bridge, method, None)
132
+ if callable(function):
133
+ setattr(bridge, method, self.wrap(function))
134
+
135
+ def release_if_idle(self):
136
+ if self.bridges_closed and self.calls == 0:
137
+ registry = _cq_sys.__dict__.get("_cfquant_pipe_strategies", {})
138
+ key = (self.descriptor.get("runtime_key"), self.descriptor.get("role"))
139
+ lock = _cq_sys.__dict__.setdefault("_cfquant_pipe_strategies_lock", _cq_threading.RLock())
140
+ with lock:
141
+ if registry.get(key) is self:
142
+ registry.pop(key, None)
143
+
144
+ def close(self, context=None):
145
+ with self.mutex:
146
+ if not self.active:
147
+ return
148
+ self.active = False
149
+ self.approved = False
150
+ try:
151
+ if self.original_stop is not None:
152
+ self.original_stop(context)
153
+ else:
154
+ bridges = list(self.attached_bridges)
155
+ for name in ("_normal_bridge", "_trade_bridge"):
156
+ bridge = self.namespace.get(name)
157
+ if bridge is not None and bridge not in bridges:
158
+ bridges.append(bridge)
159
+ for bridge in bridges:
160
+ bridge.close()
161
+ with self.mutex:
162
+ self.bridges_closed = True
163
+ self.release_if_idle()
164
+ except Exception as error:
165
+ self.failure = str(error)
166
+ print("cfquant strategy shutdown failed: %s" % error)
@@ -2,8 +2,9 @@
2
2
 
3
3
  import ast
4
4
  import copy
5
- import hashlib
6
- import json
5
+ import hashlib
6
+ import io
7
+ import json
7
8
  import os
8
9
  from pathlib import Path
9
10
  import re
@@ -319,14 +320,36 @@ def _resolve_account_binding(root, document, account_id, account_type, supplied=
319
320
  return None
320
321
 
321
322
 
323
+ def _ascii_entry_source(source):
324
+ """Preserve executable source through QMT's possible Windows ANSI recoding."""
325
+ tokens = []
326
+ for token in tokenize.generate_tokens(io.StringIO(source).readline):
327
+ if not token.string.isascii():
328
+ if token.type == tokenize.STRING:
329
+ # Escape string values, not the whole program. In particular,
330
+ # raw strings must retain their literal backslashes.
331
+ prefix = "u" if token.string[:1].lower() == "u" else ""
332
+ token = token._replace(string=prefix + ascii(ast.literal_eval(token.string)))
333
+ elif token.type == tokenize.COMMENT:
334
+ token = token._replace(string=token.string.encode("ascii", "backslashreplace").decode("ascii"))
335
+ tokens.append(token)
336
+ return tokenize.untokenize(tokens)
337
+
338
+
322
339
  def managed_source(source_path, identity_path, identity, descriptor):
323
340
  source = read_entry_source(source_path)
341
+ fileless = normalize_strategy_mode(descriptor["mode"]) in ("ctypes", "lite")
324
342
  lines = source.splitlines(keepends=True)
325
343
  tree = ast.parse(source)
326
344
  replacements = []
327
345
  constants = {"DEFAULT_ACCOUNT_ID": identity["account_id"], "DEFAULT_ACCOUNT_TYPE": identity["account_type"],
328
346
  "USER_BRIDGE_ID": identity["bridge_id"], "BRIDGE_ID": identity["bridge_id"],
329
- "QMT_MARKET": identity.get("market", "")}
347
+ "QMT_MARKET": identity.get("market", "")}
348
+ if fileless:
349
+ config = dict(identity)
350
+ config["managed_strategy"] = {key: descriptor[key] for key in ("mode", "generation", "role")}
351
+ config["managed_strategy"]["runtime_key"] = descriptor.get("runtime_key") or _digest(str(identity_path))[:24]
352
+ constants["EMBEDDED_RUNTIME_CONFIG"] = config
330
353
  seen = set()
331
354
  for node in tree.body:
332
355
  if isinstance(node, ast.Assign) and len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
@@ -334,7 +357,7 @@ def managed_source(source_path, identity_path, identity, descriptor):
334
357
  if name in constants and (name not in seen or name == "QMT_MARKET"):
335
358
  replacements.append((node.lineno - 1, node.end_lineno, name + " = " + ascii(constants[name]) + "\n"))
336
359
  seen.add(name)
337
- elif isinstance(node, ast.FunctionDef) and node.name == "_runtime_config_paths":
360
+ elif not fileless and isinstance(node, ast.FunctionDef) and node.name == "_runtime_config_paths":
338
361
  replacements.append((node.lineno - 1, node.end_lineno,
339
362
  "def _runtime_config_paths():\n return [%s]\n" % ascii(str(identity_path))))
340
363
  elif isinstance(node, ast.FunctionDef) and node.name == "_env_allows_runtime_override":
@@ -345,36 +368,84 @@ def managed_source(source_path, identity_path, identity, descriptor):
345
368
  lines[0] = "# coding: utf-8\n"
346
369
  source = "".join(lines)
347
370
  ast.parse(source, feature_version=(3, 6))
371
+ if fileless:
372
+ if "EMBEDDED_RUNTIME_CONFIG" not in seen:
373
+ raise ValueError("QMT Pipe entry does not support embedded account configuration; update the entry source.")
374
+ # Deploy the entry itself. No file lease, exec wrapper, or surrounding try block.
375
+ generated = _ascii_entry_source(source)
376
+ generated.encode("ascii")
377
+ ast.parse(generated, feature_version=(3, 6))
378
+ return generated
348
379
  helper = Path(__file__).with_name("qmt_strategy_runtime.py").read_text(encoding="utf-8")
349
380
  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
381
  embedded = "\n".join(" " + ascii(line) for line in source_lines)
382
+ entry = " exec(compile((\n%s\n ), %s, 'exec'), globals())\n" % (embedded, ascii(str(source_path)))
383
+ description = "# Embedded entry: %d lines, preserved line by line below.\n" % len(source_lines)
354
384
  header = ("# coding: utf-8\n# cfquant managed strategy\n"
355
385
  + "# Mode: " + ascii(descriptor["mode"]) + "\n"
356
386
  + "# Entry: " + ascii(Path(source_path).name) + "\n"
357
- + "# Embedded entry: %d lines, preserved line by line below.\n" % len(source_lines)
387
+ + description
358
388
  + "# The lease wrapper is shared by all modes.\n\n")
359
389
  generated = (header + helper + "\n_cq_descriptor = " + ascii(descriptor)
360
390
  + "\n_cq_guard = _CqStrategyLease(_cq_descriptor).acquire()\n"
361
391
  + "_cq_guard.namespace = globals()\ntry:\n"
362
- + " exec(compile((\n%s\n ), %s, 'exec'), globals())\n" % (embedded, ascii(str(source_path)))
392
+ + entry
363
393
  + " for _cq_name in ('_cf_bridge', '_normal_bridge', '_trade_bridge'):\n"
364
394
  + " if globals().get(_cq_name) is not None:\n"
365
395
  + " globals()[_cq_name].account_type = %s\n" % ascii(identity["account_type"])
366
396
  + " _cq_guard.bind(globals())\nexcept BaseException as _cq_error:\n"
367
397
  + " _cq_guard.failure = str(_cq_error)\n _cq_guard.close()\n raise\n")
368
- # QMT may recode source through the Windows ANSI code page before compiling.
369
- generated.encode("ascii")
370
- return generated
371
-
372
-
373
- class QmtStrategyManager:
374
- def __init__(self, state_path, scripts_dir, process_checker=qmt_is_running):
398
+ # QMT may recode source through the Windows ANSI code page before compiling.
399
+ generated.encode("ascii")
400
+ ast.parse(generated, feature_version=(3, 6))
401
+ return generated
402
+
403
+
404
+ def configured_pipe_source(source, identity):
405
+ """Embed a standalone entry's config while still in the external deployer."""
406
+ lines = source.splitlines(keepends=True)
407
+ for node in ast.parse(source).body:
408
+ if (isinstance(node, ast.Assign) and len(node.targets) == 1
409
+ and isinstance(node.targets[0], ast.Name) and node.targets[0].id == "EMBEDDED_RUNTIME_CONFIG"):
410
+ lines[node.lineno - 1:node.end_lineno] = ["EMBEDDED_RUNTIME_CONFIG = " + ascii(identity) + "\n"]
411
+ return "".join(lines)
412
+ raise ValueError("QMT Pipe entry is missing EMBEDDED_RUNTIME_CONFIG")
413
+
414
+
415
+ class PipeStrategyController:
416
+ """External Web-side client; QMT receives desired state without opening files."""
417
+ def __init__(self):
418
+ self.clients = {}
419
+
420
+ def __call__(self, identity, desired):
421
+ from .channels import channels_for_bridge
422
+ from .pipe_client import PipeRpcClient
423
+ channel = ((identity.get("channels") or {}).get("normal") or identity.get("normal_channel")
424
+ or channels_for_bridge(identity.get("bridge_id"))["normal"])
425
+ key = (identity.get("pipe_name"), channel)
426
+ client = self.clients.get(key)
427
+ if client is None:
428
+ client = self.clients[key] = PipeRpcClient(pipe_name=key[0], request_channel=channel,
429
+ timeout=0.5, connect_timeout_ms=150)
430
+ try:
431
+ return client.request("cfquant.strategy.control", desired)
432
+ except Exception:
433
+ self.clients.pop(key, None)
434
+ client.close()
435
+ raise
436
+
437
+ def close(self):
438
+ for client in self.clients.values():
439
+ client.close()
440
+ self.clients.clear()
441
+
442
+
443
+ class QmtStrategyManager:
444
+ def __init__(self, state_path, scripts_dir, process_checker=qmt_is_running, runtime_request=None):
375
445
  self.path = Path(state_path)
376
446
  self.scripts_dir = Path(scripts_dir)
377
- self.process_checker = process_checker
447
+ self.process_checker = process_checker
448
+ self.runtime_request = runtime_request
378
449
  self.lock = threading.RLock()
379
450
  self.event = threading.Event()
380
451
  self.stopping = threading.Event()
@@ -562,7 +633,7 @@ class QmtStrategyManager:
562
633
  role.pop("account_key", None)
563
634
  fingerprint = _digest([source_group, mode, [
564
635
  hashlib.sha256(Path(__file__).with_name(filename).read_bytes()).hexdigest()
565
- for filename in ("qmt_strategy_runtime.py", "qmt_strategy_package.py", "qmt_strategy_deploy.py")]])
636
+ for filename in ("qmt_strategy_runtime.py", "qmt_pipe_runtime.py", "qmt_strategy_package.py", "qmt_strategy_deploy.py")]])
566
637
  if not force and previous.get("fingerprint") == fingerprint and previous.get("enabled"):
567
638
  previous["account_key"] = row["account_key"]
568
639
  self._prepare_generation_transition(previous)
@@ -601,11 +672,18 @@ class QmtStrategyManager:
601
672
  if isinstance(role, dict) and role.get("name")
602
673
  )
603
674
  retired.difference_update(current_names)
604
- job = dict(group, account_key=row["account_key"], account_id=row["account_id"],
675
+ job = dict(group, account_key=row["account_key"], account_id=row["account_id"],
605
676
  account_type=row["account_type"], mode=mode, settings=settings,
606
677
  enabled=True, generation=generation, fingerprint=fingerprint,
607
678
  control_path=str(directory / "desired.json"),
608
- retired=sorted(retired))
679
+ retired=sorted(retired))
680
+ current_endpoints = {self._role_endpoint(role) for role in job["roles"]}
681
+ old_endpoints = {}
682
+ for old_role in previous.get("roles", []) + previous.get("retired_roles", []):
683
+ endpoint = self._role_endpoint(old_role)
684
+ if endpoint not in current_endpoints:
685
+ old_endpoints[endpoint] = {"identity": old_role["identity"], "role": old_role["role"]}
686
+ job["retired_roles"] = list(old_endpoints.values())
609
687
  for role in job["roles"]:
610
688
  role["name"] = role_names[role["role"]]
611
689
  # Every new generation needs its own imported container.
@@ -616,7 +694,8 @@ class QmtStrategyManager:
616
694
  role["identity_path"] = str(directory / (role["name"] + ".json"))
617
695
  _write_json(role["identity_path"], role["identity"])
618
696
  role["runtime_status_path"] = str(directory / (role["name"] + ".status.json"))
619
- descriptor = {"mode": job["mode"], "generation": generation, "role": role["role"],
697
+ descriptor = {"mode": job["mode"], "generation": generation, "role": role["role"],
698
+ "runtime_key": _digest([os.path.normcase(job["root"]), job["account_id"]])[:24],
620
699
  "control_path": job["control_path"], "lease_dir": str(directory / "leases"),
621
700
  "runtime_status_path": role["runtime_status_path"]}
622
701
  source = managed_source(Path(role["source"]), role["identity_path"], role["identity"], descriptor)
@@ -642,14 +721,45 @@ class QmtStrategyManager:
642
721
  self.event.set()
643
722
  return self.status(row["account_key"])
644
723
 
645
- def _control(self, job):
724
+ def _control(self, job):
725
+ _write_json(job["control_path"], self._desired(job))
726
+
727
+ def _desired(self, job):
646
728
  generation = str(job.get("control_generation") or job.get("generation") or "")
647
729
  payload = {"enabled": bool(job.get("enabled")), "generation": generation, "mode": job.get("mode", "")}
648
730
  accepted = [item for item in _generation_values(job.get("accepted_generations"))
649
731
  if item and item != generation]
650
732
  if accepted:
651
733
  payload["accepted_generations"] = accepted
652
- _write_json(job["control_path"], payload)
734
+ payload["runtime_key"] = _digest([os.path.normcase(job["root"]), job["account_id"]])[:24]
735
+ return payload
736
+
737
+ def _sync_pipe_runtime(self, job):
738
+ if self.runtime_request is None:
739
+ return
740
+ desired = self._desired(job)
741
+ for role in job.get("retired_roles", []):
742
+ try:
743
+ self.runtime_request(role["identity"], dict(desired, enabled=False))
744
+ except Exception:
745
+ pass
746
+ for role in job["roles"]:
747
+ try:
748
+ report = self.runtime_request(role["identity"], desired)
749
+ if (isinstance(report, dict) and report.get("runtime_key") == desired["runtime_key"]
750
+ and report.get("role") == role["role"]):
751
+ # Only the external manager writes status files.
752
+ _write_json(role["runtime_status_path"], report)
753
+ except Exception:
754
+ # A stopped/not-yet-imported strategy has no Pipe endpoint.
755
+ # Existing reports expire normally in _observe_runtime.
756
+ pass
757
+
758
+ @staticmethod
759
+ def _role_endpoint(role):
760
+ identity = role.get("identity") or {}
761
+ return (identity.get("pipe_name"), (identity.get("channels") or {}).get("normal")
762
+ or identity.get("normal_channel") or identity.get("bridge_id"))
653
763
 
654
764
  def reconcile(self, accounts):
655
765
  with self.lock:
@@ -718,9 +828,11 @@ class QmtStrategyManager:
718
828
 
719
829
  def _advance(self, job):
720
830
  root = qmt_root(job["root"], validate=True)
721
- if self.process_checker(root):
722
- self._observe_runtime(job)
723
- return
831
+ if self.process_checker(root):
832
+ self._sync_pipe_runtime(job)
833
+ self._observe_runtime(job)
834
+ return
835
+ job["retired_roles"] = []
724
836
  if (job.get("state") in ("configured", "running", "waiting_start", "waiting_manual_start")
725
837
  and all(role.get("imported") for role in job["roles"])):
726
838
  self._state(job, "configured")
@@ -916,8 +1028,11 @@ class QmtStrategyManager:
916
1028
  self.event.wait(2.0)
917
1029
  self.event.clear()
918
1030
 
919
- def close(self):
1031
+ def close(self):
920
1032
  self.stopping.set()
921
1033
  self.event.set()
922
- if self.worker:
923
- self.worker.join(timeout=5.0)
1034
+ if self.worker:
1035
+ self.worker.join(timeout=5.0)
1036
+ close = getattr(self.runtime_request, "close", None)
1037
+ if callable(close) and not (self.worker and self.worker.is_alive()):
1038
+ close()
@@ -2,6 +2,7 @@
2
2
 
3
3
  import functools as _cq_functools
4
4
  import glob as _cq_glob
5
+ import io as _cq_io
5
6
  import json as _cq_json
6
7
  import os as _cq_os
7
8
  import threading as _cq_threading
@@ -9,7 +10,7 @@ import time as _cq_time
9
10
 
10
11
 
11
12
  def _cq_lock(path):
12
- stream = open(path, "a+b")
13
+ stream = _cq_io.open(path, "a+b")
13
14
  try:
14
15
  stream.seek(0)
15
16
  if _cq_os.name == "nt":
@@ -62,7 +63,7 @@ class _CqStrategyLease(object):
62
63
  try:
63
64
  payload = {"state": state, "error": error, "generation": self.descriptor["generation"],
64
65
  "pid": _cq_os.getpid(), "updated_at": _cq_time.time()}
65
- with open(temporary, "w", encoding="utf-8") as stream:
66
+ with _cq_io.open(temporary, "w", encoding="utf-8") as stream:
66
67
  _cq_json.dump(payload, stream, ensure_ascii=True)
67
68
  _cq_os.replace(temporary, path)
68
69
  self.last_report = payload["updated_at"]
@@ -75,10 +76,18 @@ class _CqStrategyLease(object):
75
76
  except OSError:
76
77
  pass
77
78
 
79
+ def _read_control(self):
80
+ # QMT can supply its own open() in the strategy namespace. Use the
81
+ # standard-library IO API, as the standalone Lite entry already does.
82
+ with _cq_io.open(self.descriptor["control_path"], encoding="utf-8") as stream:
83
+ desired = _cq_json.load(stream)
84
+ if not isinstance(desired, dict):
85
+ raise ValueError("control file must contain a JSON object")
86
+ return desired
87
+
78
88
  def allowed(self):
79
89
  try:
80
- with open(self.descriptor["control_path"], encoding="utf-8") as stream:
81
- desired = _cq_json.load(stream)
90
+ desired = self._read_control()
82
91
  accepted = desired.get("accepted_generations")
83
92
  if not isinstance(accepted, (list, tuple, set)):
84
93
  accepted = ()
@@ -87,13 +96,12 @@ class _CqStrategyLease(object):
87
96
  return (desired.get("enabled") is True
88
97
  and self.descriptor["generation"] in allowed_generations
89
98
  and desired.get("mode") == self.descriptor["mode"])
90
- except (OSError, ValueError):
99
+ except (OSError, ValueError, TypeError):
91
100
  return False
92
101
 
93
102
  def _denial_reason(self):
94
103
  try:
95
- with open(self.descriptor["control_path"], encoding="utf-8") as stream:
96
- desired = _cq_json.load(stream)
104
+ desired = self._read_control()
97
105
  if desired.get("enabled") is not True:
98
106
  return "control_enabled=false"
99
107
  if desired.get("mode") != self.descriptor["mode"]:
@@ -107,8 +115,9 @@ class _CqStrategyLease(object):
107
115
  if self.descriptor["generation"] not in allowed_generations:
108
116
  return "generation_mismatch expected=%s actual=%s" % (
109
117
  desired.get("generation"), self.descriptor["generation"])
110
- except (OSError, ValueError, TypeError):
111
- return "control_file_unavailable"
118
+ except (OSError, ValueError, TypeError) as error:
119
+ return "control_file_unavailable path=%s error=%s: %s" % (
120
+ ascii(self.descriptor.get("control_path")), type(error).__name__, str(error))
112
121
  return "control_state_changed"
113
122
 
114
123
  def acquire(self, timeout=10.0):
@@ -126,7 +135,7 @@ class _CqStrategyLease(object):
126
135
  _cq_unlock(probe)
127
136
  continue
128
137
  try:
129
- with open(path + ".json", encoding="utf-8") as stream:
138
+ with _cq_io.open(path + ".json", encoding="utf-8") as stream:
130
139
  owner = _cq_json.load(stream)
131
140
  compatible = owner.get("generation") == self.descriptor["generation"]
132
141
  except (OSError, ValueError):
@@ -137,7 +146,7 @@ class _CqStrategyLease(object):
137
146
  if not busy and self.allowed():
138
147
  self.lease = _cq_lock(own_path)
139
148
  if self.lease is not None:
140
- with open(own_path + ".json", "w", encoding="utf-8") as stream:
149
+ with _cq_io.open(own_path + ".json", "w", encoding="utf-8") as stream:
141
150
  _cq_json.dump(self.descriptor, stream, ensure_ascii=True)
142
151
  self.active = True
143
152
  return self