cfquant 0.2.58__tar.gz → 0.2.59__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 (232) hide show
  1. {cfquant-0.2.58 → cfquant-0.2.59}/MANIFEST.in +1 -0
  2. {cfquant-0.2.58 → cfquant-0.2.59}/PKG-INFO +1 -1
  3. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/qmt_component_login.py +3 -2
  4. cfquant-0.2.59/cfquant/qmt_strategy_agent.js +131 -0
  5. cfquant-0.2.59/cfquant/qmt_strategy_recovery.py +480 -0
  6. cfquant-0.2.59/cfquant/tests/test_qmt_strategy_recovery.py +312 -0
  7. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/version.py +2 -2
  8. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant.egg-info/PKG-INFO +1 -1
  9. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant.egg-info/SOURCES.txt +3 -0
  10. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant_web_server.py +11 -1
  11. {cfquant-0.2.58 → cfquant-0.2.59}/pyproject.toml +2 -2
  12. {cfquant-0.2.58 → cfquant-0.2.59}/qmt_scripts/CFQUANT_LITE.py +1 -1
  13. {cfquant-0.2.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/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.58 → cfquant-0.2.59}/web_dashboard/app.js +1 -1
  16. {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/index.html +2 -2
  17. {cfquant-0.2.58 → cfquant-0.2.59}/LICENSE +0 -0
  18. {cfquant-0.2.58 → cfquant-0.2.59}/LTtx/__init__.py +0 -0
  19. {cfquant-0.2.58 → cfquant-0.2.59}/LTtx/tx/Config.txt +0 -0
  20. {cfquant-0.2.58 → cfquant-0.2.59}/LTtx/tx/LTtx_server.py +0 -0
  21. {cfquant-0.2.58 → cfquant-0.2.59}/LTtx/tx/__init__.py +0 -0
  22. {cfquant-0.2.58 → cfquant-0.2.59}/LTtx/tx/tx.py +0 -0
  23. {cfquant-0.2.58 → cfquant-0.2.59}/README.md +0 -0
  24. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/__init__.py +0 -0
  25. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/_editable_install.py +0 -0
  26. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/account_routing.py +0 -0
  27. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/algo.py +0 -0
  28. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/algo_broker.py +0 -0
  29. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/batch_orders.py +0 -0
  30. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/build_info.py +0 -0
  31. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/callback_dispatcher.py +0 -0
  32. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/cftrader.py +0 -0
  33. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/channels.py +0 -0
  34. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/cli.py +0 -0
  35. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/client.py +0 -0
  36. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/config.py +0 -0
  37. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/diagnostics.py +0 -0
  38. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/flow_control.py +0 -0
  39. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/hub.py +0 -0
  40. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/level2.py +0 -0
  41. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/log_management.py +0 -0
  42. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/logging_i18n.py +0 -0
  43. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/management.py +0 -0
  44. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/normal_bridge.py +0 -0
  45. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/order_identity.py +0 -0
  46. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/order_meta.py +0 -0
  47. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/pipe_bridge.py +0 -0
  48. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/pipe_client.py +0 -0
  49. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/pipe_hub.py +0 -0
  50. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/pipe_transport.py +0 -0
  51. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/protocol.py +0 -0
  52. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/qmt_binding_settings.py +0 -0
  53. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/qmt_bridge.py +0 -0
  54. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/qmt_component_agent.js +0 -0
  55. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/qmt_credentials.py +0 -0
  56. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/qmt_dependencies.py +0 -0
  57. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/qmt_pipe_runtime.py +0 -0
  58. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/qmt_strategy_deploy.py +0 -0
  59. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/qmt_strategy_package.py +0 -0
  60. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/qmt_strategy_runtime.py +0 -0
  61. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/qmt_update.py +0 -0
  62. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/quote_history.py +0 -0
  63. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/quote_leases.py +0 -0
  64. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/quote_service.py +0 -0
  65. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/remote_adapter.py +0 -0
  66. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/remote_management.py +0 -0
  67. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/remote_supervisor.py +0 -0
  68. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/rpc_timing.py +0 -0
  69. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/runtime_report.py +0 -0
  70. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/shared_quotes.py +0 -0
  71. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/stock_connect.py +0 -0
  72. {cfquant-0.2.58 → cfquant-0.2.59}/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
  73. {cfquant-0.2.58 → cfquant-0.2.59}/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
  74. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/12_/350/256/242/345/215/225/345/244/207/346/263/250/346/265/213/350/257/225.py" +0 -0
  75. {cfquant-0.2.58 → cfquant-0.2.59}/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
  76. {cfquant-0.2.58 → cfquant-0.2.59}/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
  77. {cfquant-0.2.58 → cfquant-0.2.59}/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
  78. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/16_/345/220/257/345/212/250/345/256/211/350/243/205/346/265/213/350/257/225.py" +0 -0
  79. {cfquant-0.2.58 → cfquant-0.2.59}/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
  80. {cfquant-0.2.58 → cfquant-0.2.59}/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
  81. {cfquant-0.2.58 → cfquant-0.2.59}/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
  82. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/1_/350/241/214/346/203/205/346/216/245/346/224/266/346/265/213/350/257/225.py" +0 -0
  83. {cfquant-0.2.58 → cfquant-0.2.59}/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
  84. {cfquant-0.2.58 → cfquant-0.2.59}/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
  85. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/22_/350/277/236/346/216/245/350/257/212/346/226/255/346/265/213/350/257/225.py" +0 -0
  86. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/23_/347/244/272/344/276/213/351/205/215/347/275/256/346/265/213/350/257/225.py" +0 -0
  87. {cfquant-0.2.58 → cfquant-0.2.59}/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
  88. {cfquant-0.2.58 → cfquant-0.2.59}/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
  89. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/26_/345/205/250/346/216/250/350/256/242/351/230/205/346/265/213/350/257/225.py" +0 -0
  90. {cfquant-0.2.58 → cfquant-0.2.59}/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
  91. {cfquant-0.2.58 → cfquant-0.2.59}/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
  92. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/29_Level2/350/241/214/346/203/205/346/265/213/350/257/225.py" +0 -0
  93. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/2_/346/225/260/346/215/256/350/216/267/345/217/226/346/265/213/350/257/225.py" +0 -0
  94. {cfquant-0.2.58 → cfquant-0.2.59}/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
  95. {cfquant-0.2.58 → cfquant-0.2.59}/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
  96. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/32_/345/274/202/346/255/245/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  97. {cfquant-0.2.58 → cfquant-0.2.59}/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
  98. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/34_/345/205/250/346/216/250/345/206/205/351/203/250/345/273/266/350/277/237/346/265/213/350/257/225.py" +0 -0
  99. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/3_/346/225/260/346/215/256/344/270/213/350/275/275/346/265/213/350/257/225.py" +0 -0
  100. {cfquant-0.2.58 → cfquant-0.2.59}/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
  101. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/5_/347/234/237/345/256/236/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  102. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/6_/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
  103. {cfquant-0.2.58 → cfquant-0.2.59}/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
  104. {cfquant-0.2.58 → cfquant-0.2.59}/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
  105. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/9_/345/217/257/346/222/244/350/256/242/345/215/225/346/265/213/350/257/225.py" +0 -0
  106. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/_helpers.py +0 -0
  107. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/readme.md +0 -0
  108. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_account_query_timing.py +0 -0
  109. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_advanced_parity.py +0 -0
  110. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_algo.py +0 -0
  111. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_algo_broker.py +0 -0
  112. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_api_tester_browser.py +0 -0
  113. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_api_tester_web.py +0 -0
  114. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
  115. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_binding_live_refresh_browser.py +0 -0
  116. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_binding_runtime_browser.py +0 -0
  117. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_callback_recovery.py +0 -0
  118. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_callback_requests.py +0 -0
  119. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_cftrader.py +0 -0
  120. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_cftrader_tutorial.py +0 -0
  121. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_cftrader_web.py +0 -0
  122. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_credit_compat.py +0 -0
  123. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_dataframe_encoding.py +0 -0
  124. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_json_serialization.py +0 -0
  125. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_latency_flow_control.py +0 -0
  126. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_lite_account_routing.py +0 -0
  127. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_lite_deployment_paths.py +0 -0
  128. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_lite_parity.py +0 -0
  129. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_lite_qmt_dispatch.py +0 -0
  130. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_local_trading_calendar.py +0 -0
  131. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_log_management.py +0 -0
  132. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_management.py +0 -0
  133. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_management_batch.py +0 -0
  134. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_market_data_fallback.py +0 -0
  135. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_market_timing.py +0 -0
  136. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_order_identity.py +0 -0
  137. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_order_time_callback.py +0 -0
  138. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_project_updater.py +0 -0
  139. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_python_api_reference.py +0 -0
  140. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_qmt_auto_login_browser.py +0 -0
  141. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_qmt_auto_login_integration.py +0 -0
  142. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_qmt_binding_settings.py +0 -0
  143. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_qmt_component_login.py +0 -0
  144. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_qmt_dependencies.py +0 -0
  145. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_qmt_fileless.py +0 -0
  146. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_qmt_strategy_browser.py +0 -0
  147. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_qmt_strategy_deploy.py +0 -0
  148. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_qmt_strategy_web.py +0 -0
  149. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_qmt_update.py +0 -0
  150. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_quote_latency.py +0 -0
  151. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_quote_latency_encoding.py +0 -0
  152. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_quote_service.py +0 -0
  153. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_remote_management.py +0 -0
  154. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_remote_management_browser.py +0 -0
  155. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_remote_version_check.py +0 -0
  156. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
  157. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_shared_quotes.py +0 -0
  158. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_shared_quotes_generated.py +0 -0
  159. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_startup_recovery.py +0 -0
  160. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_stock_connect.py +0 -0
  161. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_stock_connect_browser.py +0 -0
  162. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_subscription_congestion.py +0 -0
  163. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_table_conversion.py +0 -0
  164. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_trade_strategy_callback.py +0 -0
  165. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_trader_subscription_recovery.py +0 -0
  166. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_trading_dates.py +0 -0
  167. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_trading_monitor.py +0 -0
  168. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_trading_monitor_browser.py +0 -0
  169. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_tutorial_reader.py +0 -0
  170. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_tx_auto_import.py +0 -0
  171. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tests/test_tx_shutdown.py +0 -0
  172. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/trading_calendar.py +0 -0
  173. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/trading_monitor.py +0 -0
  174. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tx.py +0 -0
  175. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/tx_trade_bridge.py +0 -0
  176. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/xtconstant.py +0 -0
  177. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/xtdata.py +0 -0
  178. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/xttrader.py +0 -0
  179. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant/xttype.py +0 -0
  180. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant.egg-info/dependency_links.txt +0 -0
  181. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant.egg-info/entry_points.txt +0 -0
  182. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant.egg-info/requires.txt +0 -0
  183. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant.egg-info/top_level.txt +0 -0
  184. {cfquant-0.2.58 → cfquant-0.2.59}/cfquant_pipe_hub.py +0 -0
  185. {cfquant-0.2.58 → cfquant-0.2.59}/qmt_scripts/CFQUANT.py +0 -0
  186. {cfquant-0.2.58 → cfquant-0.2.59}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +0 -0
  187. {cfquant-0.2.58 → cfquant-0.2.59}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
  188. {cfquant-0.2.58 → cfquant-0.2.59}/qmt_scripts/__init__.py +0 -0
  189. {cfquant-0.2.58 → cfquant-0.2.59}/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
  190. {cfquant-0.2.58 → cfquant-0.2.59}/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
  191. {cfquant-0.2.58 → cfquant-0.2.59}/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
  192. {cfquant-0.2.58 → cfquant-0.2.59}/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
  193. {cfquant-0.2.58 → cfquant-0.2.59}/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
  194. {cfquant-0.2.58 → cfquant-0.2.59}/requirements.txt +0 -0
  195. {cfquant-0.2.58 → cfquant-0.2.59}/setup.cfg +0 -0
  196. {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/__init__.py +0 -0
  197. {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/api-tester.js +0 -0
  198. {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/avatars/amber-pulse.svg +0 -0
  199. {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/avatars/copper-grid.svg +0 -0
  200. {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/avatars/market-blue.svg +0 -0
  201. {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/avatars/rose-circuit.svg +0 -0
  202. {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/avatars/signal-green.svg +0 -0
  203. {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/avatars/slate-wave.svg +0 -0
  204. {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/avatars/teal-orbit.svg +0 -0
  205. {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/avatars/violet-node.svg +0 -0
  206. {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/cftrader-api-data.js +0 -0
  207. {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/docs.js +0 -0
  208. {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/favicon.svg +0 -0
  209. {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
  210. {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
  211. {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
  212. {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
  213. {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
  214. {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
  215. {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
  216. {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
  217. {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
  218. {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
  219. {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
  220. {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
  221. {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
  222. {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
  223. {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
  224. {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
  225. {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
  226. {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
  227. {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/python-api-data.js +0 -0
  228. {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/python-api.js +0 -0
  229. {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/remote-management.js +0 -0
  230. {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/styles.css +0 -0
  231. {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/trading-monitor.js +0 -0
  232. {cfquant-0.2.58 → cfquant-0.2.59}/web_dashboard/vendor/mermaid.min.js +0 -0
@@ -5,6 +5,7 @@ include cfquant_pipe_hub.py
5
5
 
6
6
  recursive-include cfquant *.py
7
7
  include cfquant/qmt_component_agent.js
8
+ include cfquant/qmt_strategy_agent.js
8
9
  recursive-include cfquant/tests *.py *.md
9
10
  recursive-include LTtx *.py
10
11
  include LTtx/tx/Config.txt
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cfquant
3
- Version: 0.2.58
3
+ Version: 0.2.59
4
4
  Summary: QMT local bridge and xtquant-compatible Python client.
5
5
  License: MIT
6
6
  Requires-Python: >=3.8
@@ -125,7 +125,7 @@ def select_binding(config_path, name):
125
125
 
126
126
 
127
127
  class QtLoginBackend:
128
- def __init__(self, directory, timeout=40, expected_pid=None):
128
+ def __init__(self, directory, timeout=40, expected_pid=None, *, agent_extension=""):
129
129
  try:
130
130
  from .qmt_dependencies import import_login_module
131
131
  except ImportError:
@@ -178,7 +178,8 @@ class QtLoginBackend:
178
178
  readiness.unload()
179
179
  if not ready:
180
180
  raise QmtNotReady("QMT login modules are still loading")
181
- script = self.session.create_script(Path(__file__).with_name("qmt_component_agent.js").read_text(encoding="utf-8"))
181
+ source = Path(__file__).with_name("qmt_component_agent.js").read_text(encoding="utf-8")
182
+ script = self.session.create_script(source + "\n" + agent_extension)
182
183
  script.on("message", lambda message, data: self.messages.append("agent_error") if message.get("type") == "error" else None)
183
184
  script.load()
184
185
  self.script, self.api = script, script.exports_sync
@@ -0,0 +1,131 @@
1
+ // Appended to qmt_component_agent.js. All access runs on QMT's GUI thread.
2
+ // The Python backend checks the tested Formula.dll hash before loading this.
3
+ const strategyFormula = Process.getModuleByName('Formula.dll');
4
+ const strategyXml = Process.getModuleByName('Qt5Xml.dll');
5
+ const strategyManager = fn(strategyFormula, '?instance@?$CSingletonT@VCStrategyTradeManager@TC@@@utils@@SAPEAVCStrategyTradeManager@TC@@XZ', 'pointer', []);
6
+ const strategySave = fn(strategyFormula, '?saveSettings@CStrategyTradeManager@TC@@QEAAXAEAVQDomElement@@AEAVQDomDocument@@@Z', 'void', ['pointer', 'pointer', 'pointer']);
7
+ // isStrategyRun takes a request identifier; isFomrulaRun takes a formula name.
8
+ const strategyRunning = fn(strategyFormula, '?isFomrulaRun@CStrategyTradeManager@TC@@QEAA_NAEBVQString@@@Z', 'bool', ['pointer', 'pointer']);
9
+ const strategyWhitelistReady = fn(strategyFormula, '?isConnWhiteListReady@CStrategyTradeManager@TC@@QEBA_NXZ', 'bool', ['pointer']);
10
+ const strategyRun = fn(strategyFormula, '?runStrategy@CStrategyTradeManager@TC@@QEAAXHAEBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z', 'void', ['pointer', 'int', 'pointer']);
11
+ const strategyDocCtor = fn(strategyXml, '??0QDomDocument@@QEAA@XZ', 'pointer', ['pointer']);
12
+ const strategyDocDtor = fn(strategyXml, '??1QDomDocument@@QEAA@XZ', 'void', ['pointer']);
13
+ const strategyElemDtor = fn(strategyXml, '??1QDomElement@@QEAA@XZ', 'void', ['pointer']);
14
+ const strategyCreateElem = fn(strategyXml, '?createElement@QDomDocument@@QEAA?AVQDomElement@@AEBVQString@@@Z', 'pointer', ['pointer', 'pointer', 'pointer']);
15
+ const strategyFirst = fn(strategyXml, '?firstChildElement@QDomNode@@QEBA?AVQDomElement@@AEBVQString@@@Z', 'pointer', ['pointer', 'pointer', 'pointer']);
16
+ const strategyNext = fn(strategyXml, '?nextSiblingElement@QDomNode@@QEBA?AVQDomElement@@AEBVQString@@@Z', 'pointer', ['pointer', 'pointer', 'pointer']);
17
+ const strategyNull = fn(strategyXml, '?isNull@QDomNode@@QEBA_NXZ', 'bool', ['pointer']);
18
+ const strategyAttr = fn(strategyXml, '?attribute@QDomElement@@QEBA?AVQString@@AEBV2@0@Z', 'pointer', ['pointer', 'pointer', 'pointer', 'pointer']);
19
+
20
+ function strategyString(text, action) {
21
+ const value = Memory.alloc(8);
22
+ stringCtor(value, Memory.allocUtf16String(text), text.length);
23
+ try { return action(value); } finally { stringDtor(value); }
24
+ }
25
+
26
+ function strategyAttribute(element, name) {
27
+ return strategyString(name, key => strategyString('', empty => {
28
+ const value = Memory.alloc(8);
29
+ strategyAttr(element, value, key, empty);
30
+ try { return stringData(value).readUtf16String(stringSize(value)); }
31
+ finally { stringDtor(value); }
32
+ }));
33
+ }
34
+
35
+ function strategySnapshot(root) {
36
+ if (!hasType(root, 'TC::CMainWindow')) throw new Error('QMT is not logged in');
37
+ const manager = strategyManager();
38
+ if (manager.isNull()) throw new Error('Strategy manager unavailable');
39
+ const doc = Memory.alloc(64), parent = Memory.alloc(64), models = [];
40
+ strategyDocCtor(doc);
41
+ try {
42
+ strategyString('Models', tag => strategyCreateElem(doc, parent, tag));
43
+ try {
44
+ // Serializes only into our temporary DOM; no configuration is written.
45
+ strategySave(manager, parent, doc);
46
+ strategyString('item', tag => {
47
+ let element = Memory.alloc(64);
48
+ strategyFirst(parent, element, tag);
49
+ try {
50
+ while (!strategyNull(element)) {
51
+ if (models.length >= 1000) throw new Error('Too many QMT models');
52
+ const row = {};
53
+ for (const key of ['id', 'name', 'account', 'm_strAccountKey', 'runMode',
54
+ 'startupAutorun', 'eStrategyType', 'stock', 'peroid']) {
55
+ row[key] = strategyAttribute(element, key);
56
+ }
57
+ row.running = Boolean(strategyString(row.name, name => strategyRunning(manager, name)));
58
+ models.push(row);
59
+ const next = Memory.alloc(64);
60
+ strategyNext(element, next, tag);
61
+ strategyElemDtor(element);
62
+ element = next;
63
+ }
64
+ } finally { strategyElemDtor(element); }
65
+ });
66
+ } finally { strategyElemDtor(parent); }
67
+ } finally { strategyDocDtor(doc); }
68
+ return {models, whitelist_ready: Boolean(strategyWhitelistReady(manager))};
69
+ }
70
+
71
+ rpc.exports.strategystatus = hwnd => onGui(hwnd, () => strategySnapshot(getRoot(hwnd)));
72
+ // These two additional DLL builds are verified by the Python backend before
73
+ // accessing the message dialog's content widget (Dialog.dll: setContent).
74
+ function illegalIpNotice(hwnd) {
75
+ const root = getRoot(hwnd);
76
+ if (!hasType(root, 'XP::CGlobalMessageDialog') || !isVisible(root)) return null;
77
+ const content = root.add(0xe8).readPointer();
78
+ if (content.isNull() || !childObjects(root).some(child => child.equals(content))) {
79
+ throw new Error('Unexpected QMT message content widget');
80
+ }
81
+ const value = Memory.alloc(8);
82
+ fn(Process.getModuleByName('BaseWidget.dll'), '?getText@CSimpleTextWidget@XP@@QEBA?AVQString@@XZ',
83
+ 'pointer', ['pointer', 'pointer'])(content, value);
84
+ let text;
85
+ try { text = stringData(value).readUtf16String(stringSize(value)); }
86
+ finally { stringDtor(value); }
87
+ if (!/^\[防外接\]\s*Python进程访问了网络地址\s*\[[^\]]+\]\s*[,,]\s*策略运行中止[!!]\s*$/.test(text)) return null;
88
+ return {hwnd, text};
89
+ }
90
+ rpc.exports.illegalipnotice = hwnd => onGui(hwnd, () => illegalIpNotice(hwnd));
91
+ rpc.exports.dismissillegalip = (mainHwnd, hwnd, text, expected) => onGui(mainHwnd, () => {
92
+ const models = strategySnapshot(getRoot(mainHwnd)).models;
93
+ for (const target of expected) {
94
+ const matches = models.filter(model => model.name === target.name);
95
+ if (matches.length !== 1 || Object.keys(target).some(key => matches[0][key] !== target[key])) {
96
+ throw new Error('Strategy binding changed before warning dismissal');
97
+ }
98
+ }
99
+ if (!expected.length) throw new Error('No bound strategy');
100
+ const current = illegalIpNotice(hwnd);
101
+ if (!current) return {outcome: 'already_hidden_or_changed', closed: false};
102
+ if (current.text !== text) return {outcome: 'message_changed', closed: false};
103
+ fn(Process.getModuleByName('Dialog.dll'), '?closeMessage@CGlobalMessageDialog@XP@@SAXXZ', 'void', [])();
104
+ const closed = !isVisible(getRoot(hwnd));
105
+ return {outcome: closed ? 'closed' : 'close_failed', closed};
106
+ });
107
+ rpc.exports.strategystart = (hwnd, expected) => onGui(hwnd, () => {
108
+ // Re-read on the same GUI turn as the mutation. Cached IDs are insufficient.
109
+ const snapshot = strategySnapshot(getRoot(hwnd));
110
+ const matches = snapshot.models.filter(row => row.name === expected.name);
111
+ if (matches.length !== 1) throw new Error('No unique bound strategy');
112
+ const model = matches[0];
113
+ for (const key of ['id', 'name', 'account', 'm_strAccountKey', 'runMode',
114
+ 'startupAutorun', 'eStrategyType', 'stock', 'peroid']) {
115
+ if (typeof expected[key] !== 'string' || !expected[key] || model[key] !== expected[key]) {
116
+ throw new Error('Strategy binding changed: ' + key);
117
+ }
118
+ }
119
+ if (!/^CFQ_[A-Za-z0-9_]+$/.test(model.name) || model.startupAutorun !== '1' ||
120
+ model.eStrategyType !== '4' || !/^[1-9][0-9]*$/.test(model.id) || Number(model.id) > 2147483647) {
121
+ throw new Error('Not an eligible managed strategy');
122
+ }
123
+ if (model.running) return {outcome: 'already_running', requested: false};
124
+ if (!snapshot.whitelist_ready) return {outcome: 'waiting_whitelist', requested: false};
125
+ // Empty MSVC x64 release std::string (SSO). Allowed only on the tested build.
126
+ const reason = Memory.alloc(32);
127
+ reason.writeByteArray(new Uint8Array(32));
128
+ reason.add(24).writeU64(15);
129
+ strategyRun(strategyManager(), Number(model.id), reason);
130
+ return {outcome: 'start_requested', requested: true};
131
+ });
@@ -0,0 +1,480 @@
1
+ """Recover the current binding's stopped QMT models through native Qt methods.
2
+
3
+ No mouse/keyboard events, order calls, whitelist changes, or terminal restarts.
4
+ The command line defaults to inspection; --ensure/--watch enable recovery.
5
+ """
6
+ import argparse
7
+ import copy
8
+ import datetime
9
+ import hashlib
10
+ import json
11
+ import os
12
+ from pathlib import Path
13
+ import sys
14
+ import threading
15
+ import time
16
+ import uuid
17
+
18
+ from .qmt_component_login import QtLoginBackend, QmtNotReady
19
+ from .qmt_strategy_deploy import (
20
+ _role_stock, _strategy_slot_name, account_qmt_roots,
21
+ _write_json, _digest, PipeStrategyController, normalize_strategy_mode, normalize_strategy_settings, qmt_root,
22
+ )
23
+
24
+
25
+ # Verified on Guojin simulation 2.1.33.0 / Qt 5.6.2 x64. Native ABI support is
26
+ # deliberately explicit; another build needs its own inspection and live test.
27
+ SUPPORTED_FORMULA_SHA256 = {
28
+ "673e512bab175e068cbee5fa349e33025bf564e5f57192ff3509f87dad8b8214",
29
+ }
30
+ NOTICE_DLL_SHA256 = {
31
+ "Dialog.dll": "b71c3dc77cfa830e1037ac2b7f1cc10a43aabec113a9a4c77d0fa97b34cedd67",
32
+ "BaseWidget.dll": "31fd0313f8b2b1f55f0d367121782ec3dc941a884d59a40cfc77ae155e2e861b",
33
+ }
34
+
35
+
36
+ def visible_notice_windows(pid, main_hwnd):
37
+ if os.name != "nt" or not pid or not main_hwnd:
38
+ return []
39
+ import win32gui
40
+ import win32process
41
+ result = []
42
+ def visit(hwnd, _):
43
+ if (hwnd != main_hwnd and win32gui.IsWindowVisible(hwnd)
44
+ and win32process.GetWindowThreadProcessId(hwnd)[1] == pid
45
+ and win32gui.GetClassName(hwnd).startswith("Qt")):
46
+ result.append(hwnd)
47
+ win32gui.EnumWindows(visit, None)
48
+ return result
49
+
50
+
51
+ class RecoveryJournal:
52
+ """Append UTF-8 JSON lines; flush to disk before acknowledging a warning."""
53
+ def __init__(self, directory):
54
+ self.directory = Path(directory)
55
+ self.lock = threading.Lock()
56
+
57
+ def __call__(self, event):
58
+ stamp = datetime.datetime.now().astimezone()
59
+ event = dict(event, timestamp=stamp.isoformat(timespec="milliseconds"))
60
+ with self.lock:
61
+ self.directory.mkdir(parents=True, exist_ok=True)
62
+ path = self.directory / (stamp.strftime("%Y-%m-%d") + ".jsonl")
63
+ with path.open("a", encoding="utf-8") as stream:
64
+ stream.write(json.dumps(event, ensure_ascii=False) + "\n")
65
+ stream.flush()
66
+ os.fsync(stream.fileno())
67
+
68
+
69
+ def confirm_recovery_bridge(job, pid):
70
+ controller = PipeStrategyController()
71
+ desired = dict(enabled=True, mode=job["mode"], generation=job["generation"],
72
+ runtime_key=_digest([os.path.normcase(job["root"]), job["account_id"]])[:24])
73
+ try:
74
+ for role in job["roles"]:
75
+ reply = controller(role["identity"], desired)
76
+ if (reply.get("state") != "running" or reply.get("pid") != pid
77
+ or reply.get("generation") != job["generation"]
78
+ or reply.get("role") != role["role"]
79
+ or reply.get("runtime_key") != desired["runtime_key"]):
80
+ return False
81
+ return True
82
+ finally:
83
+ controller.close()
84
+
85
+
86
+ class UnsupportedQmt(RuntimeError):
87
+ pass
88
+
89
+
90
+ class QtStrategyBackend(QtLoginBackend):
91
+ def __init__(self, root):
92
+ directory = qmt_root(root) / "bin.x64"
93
+ digest = hashlib.sha256((directory / "Formula.dll").read_bytes()).hexdigest()
94
+ if digest not in SUPPORTED_FORMULA_SHA256:
95
+ raise UnsupportedQmt("QMT 策略组件版本尚未验证,未执行自动启动")
96
+ self.notice_supported = all(hashlib.sha256((directory / name).read_bytes()).hexdigest() == digest
97
+ for name, digest in NOTICE_DLL_SHA256.items())
98
+ extension = Path(__file__).with_name("qmt_strategy_agent.js").read_text(encoding="utf-8")
99
+ super().__init__(directory, agent_extension=extension)
100
+ try:
101
+ if not self.already_logged_in():
102
+ raise QmtNotReady("QMT 尚未登录,等待主界面")
103
+ if "strategystatus" not in self.script.list_exports_sync():
104
+ raise UnsupportedQmt("QMT 策略组件接口不可用")
105
+ except Exception:
106
+ self.close()
107
+ raise
108
+
109
+ def snapshot(self):
110
+ return self.api.strategystatus(str(self.main_hwnd))["result"]
111
+
112
+ def start_strategy(self, expected):
113
+ return self.api.strategystart(str(self.main_hwnd), expected)["result"]
114
+
115
+ def notices(self):
116
+ if not self.notice_supported:
117
+ return []
118
+ result = []
119
+ for hwnd in visible_notice_windows(self.pid, self.main_hwnd):
120
+ notice = self.api.illegalipnotice(str(hwnd))["result"]
121
+ if notice:
122
+ result.append(notice)
123
+ return result
124
+
125
+ def dismiss_notice(self, notice, expected):
126
+ return self.api.dismissillegalip(str(self.main_hwnd), notice["hwnd"], notice["text"], expected)["result"]
127
+
128
+
129
+ def expected_models(job, accounts):
130
+ """Build an exact allowlist from the CURRENT binding and deployed job."""
131
+ row = accounts.get(job.get("account_key")) or {}
132
+ settings = normalize_strategy_settings(row.get("qmt_strategy"))
133
+ if (not row or not row.get("enabled", True) or not job.get("enabled")
134
+ or not settings["enabled"] or not settings["autorun"]
135
+ or str(row.get("account_id")) != job.get("account_id")
136
+ or normalize_strategy_mode(row.get("mode")) != job.get("mode")
137
+ or os.path.normcase(str(qmt_root(job["root"]))) not in account_qmt_roots(row)
138
+ or settings != job.get("settings")
139
+ or job.get("control_generation") or job.get("accepted_generations")):
140
+ return []
141
+ expected = []
142
+ for role in job.get("roles", []):
143
+ name = _strategy_slot_name(job["account_id"], job["mode"], role["role"])
144
+ if (role.get("name") != name or not role.get("model_prepared")
145
+ or role.get("reimport_required") or not role.get("model_id")
146
+ or not role.get("resolved_account_key")):
147
+ return []
148
+ expected.append({
149
+ "id": str(role["model_id"]), "name": name, "account": job["account_id"],
150
+ "m_strAccountKey": role["resolved_account_key"],
151
+ "runMode": "1" if settings["live"] else "0", "startupAutorun": "1",
152
+ "eStrategyType": "4", "stock": _role_stock(settings, role["role"]),
153
+ "peroid": str(settings["period"]),
154
+ })
155
+ return expected
156
+
157
+
158
+ def match_models(snapshot, expected):
159
+ matched = []
160
+ for target in expected:
161
+ candidates = [item for item in snapshot["models"] if item.get("name") == target["name"]]
162
+ if len(candidates) != 1 or any(candidates[0].get(k) != v for k, v in target.items()):
163
+ raise ValueError("QMT 内存策略与当前绑定不一致: " + target["name"])
164
+ matched.append(candidates[0])
165
+ return matched
166
+
167
+
168
+ class QmtStrategyRecovery:
169
+ def __init__(self, accounts_provider, *, backend_factory=QtStrategyBackend,
170
+ job_is_current=lambda job: True, clock=time.time, interval=20.0,
171
+ notice_probe=visible_notice_windows, event_sink=None, bridge_verifier=confirm_recovery_bridge):
172
+ self.accounts_provider = accounts_provider
173
+ self.backend_factory = backend_factory
174
+ self.job_is_current = job_is_current
175
+ self.clock = clock
176
+ self.interval = interval
177
+ self.notice_probe = notice_probe
178
+ self.event_sink = event_sink or RecoveryJournal(Path("runtime/logs/qmt_strategy_recovery"))
179
+ self.bridge_verifier = bridge_verifier
180
+
181
+ def _event(self, job, status, event, **details):
182
+ self.event_sink(dict(event=event, account_id=job["account_id"], account_key=job["account_key"],
183
+ mode=job["mode"], pid=status.get("pid"), generation=job["generation"],
184
+ event_id=(status.get("incident") or {}).get("id"), **details))
185
+
186
+ def _notices(self, job, status, backend, expected):
187
+ for notice in backend.notices():
188
+ if expected_models(job, self.accounts_provider()) != expected or not self.job_is_current(job):
189
+ raise ValueError("Binding changed before warning dismissal")
190
+ incident = status.get("incident") or {}
191
+ if (incident.get("text") != notice["text"] or incident.get("hwnd") != notice["hwnd"]
192
+ or incident.get("dismissed") or incident.get("handled")):
193
+ incident = dict(id=uuid.uuid4().hex, text=notice["text"], hwnd=notice["hwnd"], detected_at=self.clock())
194
+ # Detection must be durably recorded before dismissing the popup.
195
+ status["incident"] = incident
196
+ if not incident.get("recorded"):
197
+ self._event(job, status, "illegal_ip_detected", message=notice["text"])
198
+ incident["recorded"] = True
199
+ outcome = backend.dismiss_notice(notice, expected)
200
+ self._event(job, status, "illegal_ip_popup_result", **outcome)
201
+ incident["dismissed"] = outcome.get("closed") is True
202
+ incident["dismissal"] = outcome["outcome"]
203
+ status["next_check_at"] = self.clock() + 1
204
+
205
+ def _confirm_incident(self, job, status, backend, expected):
206
+ incident = status.get("incident") or {}
207
+ if not incident or incident.get("handled"):
208
+ return
209
+ status["next_check_at"] = self.clock() + (1 if self.clock() - incident["detected_at"] < 60 else self.interval)
210
+ if expected_models(job, self.accounts_provider()) != expected or not self.job_is_current(job):
211
+ return
212
+ models = match_models(backend.snapshot(), expected)
213
+ if not all(model["running"] for model in models):
214
+ return
215
+ try:
216
+ confirmed = self.bridge_verifier(job, backend.pid)
217
+ except Exception as error:
218
+ status["bridge_error"] = str(error)
219
+ return
220
+ status["bridge_confirmed"] = confirmed
221
+ if confirmed:
222
+ status.pop("bridge_error", None)
223
+ if confirmed and incident.get("dismissed"):
224
+ self._event(job, status, "illegal_ip_handled", message="非法 IP 弹窗已关闭,绑定策略及通信恢复成功",
225
+ strategies=[model["name"] for model in models], popup_closed=True, bridge_confirmed=True)
226
+ incident["handled"] = True
227
+ status.update(state="running", strategies=[{"name": m["name"], "running": True} for m in models])
228
+ status["next_check_at"] = self.clock() + self.interval
229
+
230
+ def __call__(self, job, *, inspect_only=False):
231
+ now = self.clock()
232
+ expected = expected_models(job, self.accounts_provider())
233
+ if not expected or not self.job_is_current(job):
234
+ job["recovery"] = {"state": "ineligible", "last_checked_at": now}
235
+ return job["recovery"]
236
+ previous = job.get("recovery") or {}
237
+ if (not inspect_only and now < previous.get("next_check_at", 0)
238
+ and not self.notice_probe(previous.get("pid"), previous.get("main_hwnd"))):
239
+ return previous
240
+ status = dict(previous, last_checked_at=now, next_check_at=now + self.interval)
241
+ status.pop("error", None)
242
+ backend = None
243
+ try:
244
+ backend = self.backend_factory(job["root"])
245
+ if (status.get("pid"), status.get("generation")) != (backend.pid, job["generation"]):
246
+ status = {"last_checked_at": now, "next_check_at": now + self.interval,
247
+ "pid": backend.pid, "generation": job["generation"], "attempts": {}}
248
+ models = match_models(backend.snapshot(), expected)
249
+ status["main_hwnd"] = getattr(backend, "main_hwnd", None)
250
+ if not inspect_only:
251
+ self._notices(job, status, backend, expected)
252
+ status["strategies"] = [{"name": item["name"], "running": item["running"]} for item in models]
253
+ status["bridge_confirmed"] = all(item["running"] for item in models) and all(
254
+ (role.get("control_sync") or {}).get("pid") == backend.pid
255
+ and (role.get("control_sync") or {}).get("state") == "running"
256
+ and 0 <= now - (role.get("control_sync") or {}).get("last_success_at", 0) < 10
257
+ for role in job["roles"])
258
+ stopped = [target for target, model in zip(expected, models) if not model["running"]]
259
+ if not stopped:
260
+ status["state"] = "running"
261
+ if status["bridge_confirmed"]:
262
+ status.setdefault("stable_since", now)
263
+ if now - status["stable_since"] >= 300:
264
+ status["attempts"] = {}
265
+ else:
266
+ status.pop("stable_since", None)
267
+ else:
268
+ status.pop("stable_since", None)
269
+ status["state"] = "stopped"
270
+ if not inspect_only:
271
+ attempts = dict(status.get("attempts") or {})
272
+ status["attempts"] = attempts
273
+ if any(attempts.get(target["name"], 0) >= 5 for target in stopped):
274
+ status["state"] = "retry_exhausted"
275
+ elif now < status.get("retry_after", 0):
276
+ status["state"] = "backoff"
277
+ else:
278
+ for target in stopped:
279
+ # Re-check the binding after attach/observation and before each start.
280
+ if (expected_models(job, self.accounts_provider()) != expected
281
+ or not self.job_is_current(job)):
282
+ status["state"] = "binding_changed"
283
+ break
284
+ count = attempts.get(target["name"], 0) + 1
285
+ attempts[target["name"]] = count
286
+ status["retry_after"] = now + min(300, 60 * 2 ** (count - 1))
287
+ # Reserve the attempt before a call that may time out ambiguously.
288
+ job["recovery"] = status
289
+ outcome = backend.start_strategy(target)
290
+ status["state"] = outcome["outcome"]
291
+ if outcome.get("requested"):
292
+ status["last_started_at"] = now
293
+ self._event(job, status, "strategy_start_requested", strategy=target["name"])
294
+ else:
295
+ attempts[target["name"]] -= 1
296
+ status["retry_after"] = now
297
+ if not inspect_only:
298
+ self._confirm_incident(job, status, backend, expected)
299
+ if status.get("state") in ("retry_exhausted", "backoff") and status.get("incident"):
300
+ if status.get("last_logged_state") != status["state"]:
301
+ self._event(job, status, "illegal_ip_recovery_pending", state=status["state"])
302
+ status["last_logged_state"] = status["state"]
303
+ except Exception as error:
304
+ status.update(state="unsupported" if isinstance(error, UnsupportedQmt) else "unavailable",
305
+ error=str(error), next_check_at=now + max(self.interval, 60))
306
+ if status.get("incident") and status.get("last_logged_error") != str(error):
307
+ try:
308
+ self._event(job, status, "illegal_ip_processing_failed", error=str(error))
309
+ status["last_logged_error"] = str(error)
310
+ except OSError:
311
+ pass # Preserve the original journal failure in status; never claim success.
312
+ finally:
313
+ if backend is not None:
314
+ backend.close()
315
+ job["recovery"] = status
316
+ return status
317
+
318
+
319
+ class QmtStrategyRecoveryScheduler:
320
+ """Separate worker: native GUI waits must not delay the Pipe control loop."""
321
+ def __init__(self, manager, accounts_provider, report_path, interval=20.0):
322
+ self.manager = manager
323
+ self.accounts_provider = accounts_provider
324
+ self.report_path = Path(report_path)
325
+ self.interval = interval
326
+ self.stop_event = threading.Event()
327
+ self.thread = None
328
+ self.lock = threading.RLock()
329
+ self.states = {}
330
+ try:
331
+ self.states = json.loads(self.report_path.read_text(encoding="utf-8")).get("targets", {})
332
+ except (OSError, ValueError):
333
+ pass
334
+ self.recovery = QmtStrategyRecovery(accounts_provider, job_is_current=self._current, interval=interval,
335
+ event_sink=RecoveryJournal(self.report_path.parent.parent / "logs/qmt_strategy_recovery"))
336
+
337
+ def _current(self, job):
338
+ if self.stop_event.is_set():
339
+ return False
340
+ with self.manager.lock:
341
+ accounts = self.accounts_provider()
342
+ return any(item.get("account_key") == job["account_key"]
343
+ and item.get("root") == job["root"]
344
+ and item.get("generation") == job["generation"]
345
+ and expected_models(item, accounts) == expected_models(job, accounts)
346
+ for item in self.manager.jobs.values())
347
+
348
+ def run_once(self):
349
+ with self.manager.lock:
350
+ jobs = copy.deepcopy(self.manager.jobs)
351
+ with self.lock:
352
+ states = copy.deepcopy(self.states)
353
+ result = {}
354
+ for key, job in jobs.items():
355
+ if self.stop_event.is_set():
356
+ break
357
+ job["recovery"] = states.get(key, {})
358
+ try:
359
+ result[key] = self.recovery(job)
360
+ except Exception as error:
361
+ result[key] = dict(job["recovery"], state="unavailable", error=str(error))
362
+ with self.lock:
363
+ self.states = result
364
+ _write_json(self.report_path, {"checked_at": time.time(), "targets": result})
365
+
366
+ def decorate(self, account_key, status):
367
+ status = copy.deepcopy(status)
368
+ with self.manager.lock:
369
+ keys = {job["root"]: (key, job.get("generation")) for key, job in self.manager.jobs.items()
370
+ if job.get("account_key") == account_key}
371
+ with self.lock:
372
+ for target in status.get("targets", []):
373
+ key, generation = keys.get(target.get("root"), (None, None))
374
+ recovery = copy.deepcopy(self.states.get(key, {}))
375
+ if (recovery.get("generation") not in (None, generation)
376
+ or time.time() - recovery.get("last_checked_at", 0) > 120):
377
+ recovery = {}
378
+ target["recovery"] = recovery
379
+ state = recovery.get("state")
380
+ if state in ("start_requested", "backoff", "retry_exhausted"):
381
+ target["state"] = "waiting_start"
382
+ target["message"] = {
383
+ "start_requested": "已检测到托管策略停止,已直接提交启动,等待通道恢复",
384
+ "backoff": "托管策略尚未恢复,等待冷却后重试",
385
+ "retry_exhausted": "托管策略连续启动失败,已暂停重试,请检查 QMT 提示",
386
+ }[state]
387
+ elif state == "unsupported" and target.get("state") != "running":
388
+ target["message"] = recovery.get("error", "当前 QMT 版本尚未验证策略自动恢复")
389
+ return status
390
+
391
+ def start(self):
392
+ if self.thread and self.thread.is_alive():
393
+ return
394
+ self.stop_event.clear()
395
+ def run():
396
+ while not self.stop_event.is_set():
397
+ try:
398
+ self.run_once()
399
+ except Exception:
400
+ pass # Transient config/status IO failures retry on the next interval.
401
+ self.stop_event.wait(1.0)
402
+ self.thread = threading.Thread(target=run, name="qmt-strategy-recovery", daemon=True)
403
+ self.thread.start()
404
+
405
+ def close(self):
406
+ self.stop_event.set()
407
+ if self.thread:
408
+ self.thread.join(timeout=2)
409
+
410
+
411
+ def main():
412
+ if hasattr(sys.stdout, "reconfigure"):
413
+ sys.stdout.reconfigure(encoding="utf-8")
414
+ parser = argparse.ArgumentParser(description=__doc__)
415
+ parser.add_argument("--account-id", required=True)
416
+ parser.add_argument("--config", type=Path, default=Path("runtime/config/cfquant_web_config.json"))
417
+ parser.add_argument("--jobs", type=Path, default=Path("runtime/config/qmt_strategy_jobs.json"))
418
+ action = parser.add_mutually_exclusive_group()
419
+ action.add_argument("--ensure", action="store_true")
420
+ action.add_argument("--watch", action="store_true")
421
+ parser.add_argument("--report", type=Path)
422
+ parser.add_argument("--parent-pid", type=int, help="Stop the temporary watcher when this Web process exits")
423
+ args = parser.parse_args()
424
+
425
+ def accounts():
426
+ return json.loads(args.config.read_text(encoding="utf-8"))["account_configs"]
427
+
428
+ def jobs():
429
+ return json.loads(args.jobs.read_text(encoding="utf-8"))["jobs"]
430
+
431
+ def current(job):
432
+ return any(item.get("account_key") == job["account_key"]
433
+ and item.get("root") == job["root"]
434
+ and item.get("generation") == job["generation"]
435
+ and item.get("enabled")
436
+ and expected_models(item, accounts()) == expected_models(job, accounts())
437
+ for item in jobs().values())
438
+
439
+ parent = None
440
+ if args.parent_pid:
441
+ import psutil
442
+ parent = psutil.Process(args.parent_pid)
443
+ recovery = QmtStrategyRecovery(accounts, job_is_current=lambda job: (
444
+ (parent is None or parent.is_running()) and current(job)),
445
+ event_sink=RecoveryJournal(args.config.parent.parent / "logs/qmt_strategy_recovery"))
446
+ states = {}
447
+ if args.report and args.report.exists():
448
+ states = json.loads(args.report.read_text(encoding="utf-8")).get("targets", {})
449
+ while True:
450
+ if parent is not None and not parent.is_running():
451
+ return 0
452
+ output = {"account_id": args.account_id, "checked_at": time.time(), "targets": {}}
453
+ try:
454
+ for key, original in jobs().items():
455
+ if original.get("account_id") != args.account_id:
456
+ continue
457
+ job = copy.deepcopy(original)
458
+ if key in states:
459
+ job["recovery"] = states[key]
460
+ result = recovery(job, inspect_only=not (args.ensure or args.watch))
461
+ output["targets"][key] = result
462
+ except Exception as error:
463
+ if not args.watch:
464
+ raise
465
+ output.update(targets=states, error=str(error))
466
+ states = output["targets"]
467
+ text = json.dumps(output, ensure_ascii=False, indent=2)
468
+ if args.report:
469
+ args.report.parent.mkdir(parents=True, exist_ok=True)
470
+ temporary = args.report.with_suffix(".pending")
471
+ temporary.write_text(text, encoding="utf-8")
472
+ temporary.replace(args.report)
473
+ if not args.watch:
474
+ print(text)
475
+ return 0 if states and all(s["state"] in ("running", "start_requested") for s in states.values()) else 1
476
+ time.sleep(1)
477
+
478
+
479
+ if __name__ == "__main__":
480
+ sys.exit(main())