cfquant 0.2.62__tar.gz → 0.2.63__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 (243) hide show
  1. {cfquant-0.2.62 → cfquant-0.2.63}/PKG-INFO +3 -1
  2. {cfquant-0.2.62 → cfquant-0.2.63}/README.md +2 -0
  3. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/management.py +32 -0
  4. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/qmt_component_login.py +8 -6
  5. cfquant-0.2.63/cfquant/qmt_native_files.py +145 -0
  6. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/qmt_native_worker.py +117 -12
  7. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/qmt_strategy_agent.js +8 -5
  8. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/qmt_strategy_deploy.py +33 -0
  9. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/qmt_strategy_recovery.py +115 -42
  10. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_qmt_auto_login_integration.py +24 -1
  11. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_qmt_component_login.py +21 -0
  12. cfquant-0.2.63/cfquant/tests/test_qmt_native_files.py +135 -0
  13. cfquant-0.2.63/cfquant/tests/test_qmt_native_reuse.py +135 -0
  14. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_qmt_native_worker.py +7 -0
  15. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_qmt_strategy_capabilities.py +23 -3
  16. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_qmt_strategy_deploy.py +70 -0
  17. cfquant-0.2.63/cfquant/tests/test_strategy_recovery_api.py +239 -0
  18. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/version.py +2 -2
  19. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant.egg-info/PKG-INFO +3 -1
  20. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant.egg-info/SOURCES.txt +4 -0
  21. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant_web_server.py +42 -3
  22. {cfquant-0.2.62 → cfquant-0.2.63}/pyproject.toml +1 -1
  23. {cfquant-0.2.62 → cfquant-0.2.63}/qmt_scripts/CFQUANT_LITE.py +1 -1
  24. {cfquant-0.2.62 → cfquant-0.2.63}/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
  25. {cfquant-0.2.62 → cfquant-0.2.63}/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
  26. {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/app.js +1 -1
  27. {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/index.html +2 -2
  28. {cfquant-0.2.62 → cfquant-0.2.63}/LICENSE +0 -0
  29. {cfquant-0.2.62 → cfquant-0.2.63}/LTtx/__init__.py +0 -0
  30. {cfquant-0.2.62 → cfquant-0.2.63}/LTtx/tx/Config.txt +0 -0
  31. {cfquant-0.2.62 → cfquant-0.2.63}/LTtx/tx/LTtx_server.py +0 -0
  32. {cfquant-0.2.62 → cfquant-0.2.63}/LTtx/tx/__init__.py +0 -0
  33. {cfquant-0.2.62 → cfquant-0.2.63}/LTtx/tx/tx.py +0 -0
  34. {cfquant-0.2.62 → cfquant-0.2.63}/MANIFEST.in +0 -0
  35. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/__init__.py +0 -0
  36. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/_editable_install.py +0 -0
  37. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/account_routing.py +0 -0
  38. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/algo.py +0 -0
  39. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/algo_broker.py +0 -0
  40. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/batch_orders.py +0 -0
  41. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/build_info.py +0 -0
  42. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/callback_dispatcher.py +0 -0
  43. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/cftrader.py +0 -0
  44. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/channels.py +0 -0
  45. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/cli.py +0 -0
  46. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/client.py +0 -0
  47. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/config.py +0 -0
  48. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/diagnostics.py +0 -0
  49. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/flow_control.py +0 -0
  50. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/hub.py +0 -0
  51. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/level2.py +0 -0
  52. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/log_management.py +0 -0
  53. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/logging_i18n.py +0 -0
  54. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/normal_bridge.py +0 -0
  55. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/order_identity.py +0 -0
  56. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/order_meta.py +0 -0
  57. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/pipe_bridge.py +0 -0
  58. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/pipe_client.py +0 -0
  59. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/pipe_hub.py +0 -0
  60. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/pipe_transport.py +0 -0
  61. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/protocol.py +0 -0
  62. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/qmt_binding_defaults.py +0 -0
  63. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/qmt_binding_settings.py +0 -0
  64. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/qmt_bridge.py +0 -0
  65. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/qmt_component_agent.js +0 -0
  66. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/qmt_credentials.py +0 -0
  67. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/qmt_dependencies.py +0 -0
  68. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/qmt_pipe_runtime.py +0 -0
  69. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/qmt_strategy_package.py +0 -0
  70. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/qmt_strategy_runtime.py +0 -0
  71. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/qmt_update.py +0 -0
  72. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/quote_history.py +0 -0
  73. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/quote_leases.py +0 -0
  74. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/quote_service.py +0 -0
  75. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/remote_adapter.py +0 -0
  76. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/remote_management.py +0 -0
  77. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/remote_supervisor.py +0 -0
  78. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/rpc_timing.py +0 -0
  79. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/runtime_report.py +0 -0
  80. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/shared_quotes.py +0 -0
  81. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/stock_connect.py +0 -0
  82. {cfquant-0.2.62 → cfquant-0.2.63}/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
  83. {cfquant-0.2.62 → cfquant-0.2.63}/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
  84. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/12_/350/256/242/345/215/225/345/244/207/346/263/250/346/265/213/350/257/225.py" +0 -0
  85. {cfquant-0.2.62 → cfquant-0.2.63}/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
  86. {cfquant-0.2.62 → cfquant-0.2.63}/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
  87. {cfquant-0.2.62 → cfquant-0.2.63}/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
  88. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/16_/345/220/257/345/212/250/345/256/211/350/243/205/346/265/213/350/257/225.py" +0 -0
  89. {cfquant-0.2.62 → cfquant-0.2.63}/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
  90. {cfquant-0.2.62 → cfquant-0.2.63}/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
  91. {cfquant-0.2.62 → cfquant-0.2.63}/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
  92. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/1_/350/241/214/346/203/205/346/216/245/346/224/266/346/265/213/350/257/225.py" +0 -0
  93. {cfquant-0.2.62 → cfquant-0.2.63}/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
  94. {cfquant-0.2.62 → cfquant-0.2.63}/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
  95. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/22_/350/277/236/346/216/245/350/257/212/346/226/255/346/265/213/350/257/225.py" +0 -0
  96. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/23_/347/244/272/344/276/213/351/205/215/347/275/256/346/265/213/350/257/225.py" +0 -0
  97. {cfquant-0.2.62 → cfquant-0.2.63}/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
  98. {cfquant-0.2.62 → cfquant-0.2.63}/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
  99. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/26_/345/205/250/346/216/250/350/256/242/351/230/205/346/265/213/350/257/225.py" +0 -0
  100. {cfquant-0.2.62 → cfquant-0.2.63}/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
  101. {cfquant-0.2.62 → cfquant-0.2.63}/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
  102. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/29_Level2/350/241/214/346/203/205/346/265/213/350/257/225.py" +0 -0
  103. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/2_/346/225/260/346/215/256/350/216/267/345/217/226/346/265/213/350/257/225.py" +0 -0
  104. {cfquant-0.2.62 → cfquant-0.2.63}/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
  105. {cfquant-0.2.62 → cfquant-0.2.63}/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
  106. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/32_/345/274/202/346/255/245/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  107. {cfquant-0.2.62 → cfquant-0.2.63}/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
  108. {cfquant-0.2.62 → cfquant-0.2.63}/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
  109. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/3_/346/225/260/346/215/256/344/270/213/350/275/275/346/265/213/350/257/225.py" +0 -0
  110. {cfquant-0.2.62 → cfquant-0.2.63}/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
  111. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/5_/347/234/237/345/256/236/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  112. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/6_/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
  113. {cfquant-0.2.62 → cfquant-0.2.63}/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
  114. {cfquant-0.2.62 → cfquant-0.2.63}/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
  115. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/9_/345/217/257/346/222/244/350/256/242/345/215/225/346/265/213/350/257/225.py" +0 -0
  116. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/_helpers.py +0 -0
  117. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/readme.md +0 -0
  118. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_account_query_timing.py +0 -0
  119. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_account_subscription_probe.py +0 -0
  120. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_advanced_parity.py +0 -0
  121. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_algo.py +0 -0
  122. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_algo_broker.py +0 -0
  123. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_api_tester_browser.py +0 -0
  124. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_api_tester_web.py +0 -0
  125. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
  126. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_binding_live_refresh_browser.py +0 -0
  127. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_binding_runtime_browser.py +0 -0
  128. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_callback_recovery.py +0 -0
  129. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_callback_requests.py +0 -0
  130. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_cftrader.py +0 -0
  131. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_cftrader_tutorial.py +0 -0
  132. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_cftrader_web.py +0 -0
  133. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_credit_compat.py +0 -0
  134. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_dataframe_encoding.py +0 -0
  135. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_json_serialization.py +0 -0
  136. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_latency_flow_control.py +0 -0
  137. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_lite_account_routing.py +0 -0
  138. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_lite_deployment_paths.py +0 -0
  139. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_lite_parity.py +0 -0
  140. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_lite_qmt_dispatch.py +0 -0
  141. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_local_trading_calendar.py +0 -0
  142. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_log_management.py +0 -0
  143. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_management.py +0 -0
  144. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_management_batch.py +0 -0
  145. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_market_data_fallback.py +0 -0
  146. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_market_timing.py +0 -0
  147. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_order_identity.py +0 -0
  148. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_order_time_callback.py +0 -0
  149. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_pipe_shutdown.py +0 -0
  150. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_project_updater.py +0 -0
  151. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_python_api_reference.py +0 -0
  152. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_qmt_auto_login_browser.py +0 -0
  153. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_qmt_binding_defaults.py +0 -0
  154. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_qmt_binding_settings.py +0 -0
  155. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_qmt_dependencies.py +0 -0
  156. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_qmt_fileless.py +0 -0
  157. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_qmt_strategy_browser.py +0 -0
  158. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_qmt_strategy_recovery.py +0 -0
  159. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_qmt_strategy_web.py +0 -0
  160. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_qmt_update.py +0 -0
  161. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_quote_latency.py +0 -0
  162. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_quote_latency_encoding.py +0 -0
  163. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_quote_service.py +0 -0
  164. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_remote_management.py +0 -0
  165. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_remote_management_browser.py +0 -0
  166. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_remote_version_check.py +0 -0
  167. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
  168. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_shared_quotes.py +0 -0
  169. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_shared_quotes_generated.py +0 -0
  170. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_startup_recovery.py +0 -0
  171. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_stock_connect.py +0 -0
  172. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_stock_connect_browser.py +0 -0
  173. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_subscription_congestion.py +0 -0
  174. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_table_conversion.py +0 -0
  175. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_trade_strategy_callback.py +0 -0
  176. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_trader_subscription_recovery.py +0 -0
  177. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_trading_dates.py +0 -0
  178. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_trading_monitor.py +0 -0
  179. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_trading_monitor_browser.py +0 -0
  180. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_tutorial_reader.py +0 -0
  181. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_tx_auto_import.py +0 -0
  182. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tests/test_tx_shutdown.py +0 -0
  183. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/trading_calendar.py +0 -0
  184. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/trading_monitor.py +0 -0
  185. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tx.py +0 -0
  186. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/tx_trade_bridge.py +0 -0
  187. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/xtconstant.py +0 -0
  188. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/xtdata.py +0 -0
  189. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/xttrader.py +0 -0
  190. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant/xttype.py +0 -0
  191. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant.egg-info/dependency_links.txt +0 -0
  192. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant.egg-info/entry_points.txt +0 -0
  193. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant.egg-info/requires.txt +0 -0
  194. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant.egg-info/top_level.txt +0 -0
  195. {cfquant-0.2.62 → cfquant-0.2.63}/cfquant_pipe_hub.py +0 -0
  196. {cfquant-0.2.62 → cfquant-0.2.63}/qmt_scripts/CFQUANT.py +0 -0
  197. {cfquant-0.2.62 → cfquant-0.2.63}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +0 -0
  198. {cfquant-0.2.62 → cfquant-0.2.63}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
  199. {cfquant-0.2.62 → cfquant-0.2.63}/qmt_scripts/__init__.py +0 -0
  200. {cfquant-0.2.62 → cfquant-0.2.63}/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
  201. {cfquant-0.2.62 → cfquant-0.2.63}/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
  202. {cfquant-0.2.62 → cfquant-0.2.63}/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
  203. {cfquant-0.2.62 → cfquant-0.2.63}/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
  204. {cfquant-0.2.62 → cfquant-0.2.63}/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
  205. {cfquant-0.2.62 → cfquant-0.2.63}/requirements.txt +0 -0
  206. {cfquant-0.2.62 → cfquant-0.2.63}/setup.cfg +0 -0
  207. {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/__init__.py +0 -0
  208. {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/api-tester.js +0 -0
  209. {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/avatars/amber-pulse.svg +0 -0
  210. {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/avatars/copper-grid.svg +0 -0
  211. {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/avatars/market-blue.svg +0 -0
  212. {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/avatars/rose-circuit.svg +0 -0
  213. {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/avatars/signal-green.svg +0 -0
  214. {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/avatars/slate-wave.svg +0 -0
  215. {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/avatars/teal-orbit.svg +0 -0
  216. {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/avatars/violet-node.svg +0 -0
  217. {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/cftrader-api-data.js +0 -0
  218. {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/docs.js +0 -0
  219. {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/favicon.svg +0 -0
  220. {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
  221. {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
  222. {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
  223. {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
  224. {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
  225. {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
  226. {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
  227. {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
  228. {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
  229. {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
  230. {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
  231. {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
  232. {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
  233. {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
  234. {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
  235. {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
  236. {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
  237. {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
  238. {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/python-api-data.js +0 -0
  239. {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/python-api.js +0 -0
  240. {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/remote-management.js +0 -0
  241. {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/styles.css +0 -0
  242. {cfquant-0.2.62 → cfquant-0.2.63}/web_dashboard/trading-monitor.js +0 -0
  243. {cfquant-0.2.62 → cfquant-0.2.63}/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.62
3
+ Version: 0.2.63
4
4
  Summary: QMT local bridge and xtquant-compatible Python client.
5
5
  License: MIT
6
6
  Requires-Python: >=3.8
@@ -283,6 +283,8 @@ print(result["status"], result["message"])
283
283
 
284
284
  `version_info(include_remote=False)` 只查询本地版本;`updates.check(force=True)` 只检查更新。按需更新也识别同版本号的安装包变化,首次安装包哈希未知时会执行一次更新以建立记录。真正需要更新时,默认要求先关闭相关 QMT;也可显式传入 `auto_close_qmt=True` 允许自动关闭。`restart=False` 可留待调用方重启。部分 QMT 部署失败返回 `update_incomplete`,已安装但尚未重启返回 `restart_required`;执行更新失败会抛出 `ManagementError`。
285
285
 
286
+ 外部程序复用绑定时的 `runtime` 对象,直接调用 `runtime.recover_strategy("YOUR_ACCOUNT_ID", account_type="STOCK")` 即可检测并尝试恢复已停止的 QMT 托管策略。本地托管实例由 SDK 自动读取管理凭据。也支持 `runtime.accounts.recover_strategy(...)` 和鉴权后的 `POST /api/qmt/strategies/recover`。返回值区分已提交启动、模型运行和通信确认,具体条件及示例见 [策略恢复接口](docs/第三方项目管理SDK.md#外部程序恢复-qmt-托管策略)。
287
+
286
288
  模拟/实盘参数需与实际账号一致。完整的启动策略、更新接口、重复绑定行为与错误处理见 [第三方项目管理 SDK](docs/第三方项目管理SDK.md)。
287
289
 
288
290
  ## 从 `xtquant` 迁移到 `cfquant`
@@ -258,6 +258,8 @@ print(result["status"], result["message"])
258
258
 
259
259
  `version_info(include_remote=False)` 只查询本地版本;`updates.check(force=True)` 只检查更新。按需更新也识别同版本号的安装包变化,首次安装包哈希未知时会执行一次更新以建立记录。真正需要更新时,默认要求先关闭相关 QMT;也可显式传入 `auto_close_qmt=True` 允许自动关闭。`restart=False` 可留待调用方重启。部分 QMT 部署失败返回 `update_incomplete`,已安装但尚未重启返回 `restart_required`;执行更新失败会抛出 `ManagementError`。
260
260
 
261
+ 外部程序复用绑定时的 `runtime` 对象,直接调用 `runtime.recover_strategy("YOUR_ACCOUNT_ID", account_type="STOCK")` 即可检测并尝试恢复已停止的 QMT 托管策略。本地托管实例由 SDK 自动读取管理凭据。也支持 `runtime.accounts.recover_strategy(...)` 和鉴权后的 `POST /api/qmt/strategies/recover`。返回值区分已提交启动、模型运行和通信确认,具体条件及示例见 [策略恢复接口](docs/第三方项目管理SDK.md#外部程序恢复-qmt-托管策略)。
262
+
261
263
  模拟/实盘参数需与实际账号一致。完整的启动策略、更新接口、重复绑定行为与错误处理见 [第三方项目管理 SDK](docs/第三方项目管理SDK.md)。
262
264
 
263
265
  ## 从 `xtquant` 迁移到 `cfquant`
@@ -314,6 +314,17 @@ class RuntimeManager:
314
314
  """Initialize the first binding; subsequent calls upsert that binding."""
315
315
  return self.accounts.upsert(account_id, qmt_dir, _initialize=True, **options)
316
316
 
317
+ def recover_strategy(self, account_id=None, *, account_type=None, bridge_id=None,
318
+ account_key=None, timeout=180):
319
+ """Recover a binding using the same managed service as initialize().
320
+
321
+ Reuse this RuntimeManager after binding accounts. Local managed services
322
+ automatically use home/management.key, just like account management.
323
+ """
324
+ return self.accounts.recover_strategy(account_id, account_type=account_type,
325
+ bridge_id=bridge_id, account_key=account_key,
326
+ timeout=timeout)
327
+
317
328
  def wait_ready(self, account_id, *, account_type="STOCK", bridge_id=None, timeout=120):
318
329
  deadline = time.monotonic() + timeout
319
330
  last = None
@@ -363,6 +374,27 @@ class AccountManager:
363
374
  def status(self, timeout=None):
364
375
  return self.runtime._request("GET", "/api/bindings/status", timeout=timeout)
365
376
 
377
+ def recover_strategy(self, account_id=None, *, account_type=None, bridge_id=None,
378
+ account_key=None, timeout=180):
379
+ """Check and recover this binding's stopped managed QMT strategies.
380
+
381
+ Requires enabled strategy automation and a completed deployment. Returns
382
+ running/start_requested and per-target recovery states; submitting a start
383
+ is not confirmation that the strategy or its bridge is already running.
384
+ Uses the Web service, so it also works while the QMT bridge is offline.
385
+ """
386
+ body = {key: str(value).strip() for key, value in {
387
+ "account_id": account_id, "account_type": account_type,
388
+ "bridge_id": bridge_id, "account_key": account_key,
389
+ }.items() if value is not None}
390
+ if not body.get("account_id") and not body.get("account_key"):
391
+ raise ValueError("account_id or account_key is required")
392
+ if any(not value for value in body.values()):
393
+ raise ValueError("account selectors must not be empty")
394
+ if not self.runtime.attached:
395
+ self.runtime.status() # Verify managed instance ownership first.
396
+ return self.runtime._request("POST", "/api/qmt/strategies/recover", body, timeout=timeout)
397
+
366
398
  def upsert(self, account_id, qmt_dir, *, account_type="STOCK", mode="ctypes",
367
399
  auto_start_qmt=False, deploy_strategy=True, strategy_autorun=True,
368
400
  live=True, auto_close_qmt=False, force=False, _initialize=False, **options):
@@ -310,6 +310,12 @@ class QtLoginBackend:
310
310
  self.api.acknowledge(str(self.failure_hwnd))
311
311
  self.failure_hwnd = None
312
312
 
313
+ def diagnostics(self):
314
+ return dict(minimized_before=self.minimized_before,
315
+ foreground_before=self.foreground_before,
316
+ foreground_after=self.gui.GetForegroundWindow(),
317
+ script_errors=list(self.messages))
318
+
313
319
  def close(self):
314
320
  try:
315
321
  from .qmt_dependencies import import_login_module
@@ -341,17 +347,13 @@ def main():
341
347
  if not 1 <= args.timeout <= 120:
342
348
  parser.error("timeout must be between 1 and 120 seconds")
343
349
  row, directory = select_binding(args.config, args.binding)
344
- backend = QtLoginBackend(directory, args.timeout)
350
+ backend = IsolatedLoginBackend(directory, args.timeout)
345
351
  try:
346
352
  backend.select_account((row.get("qmt_auto_login") or {}).get("login_account", ""), replace=True)
347
353
  def read_password():
348
354
  return getpass.getpass("QMT password (hidden): ") if args.prompt else sys.stdin.readline().rstrip("\r\n")
349
355
  result = auto_login(backend, read_password)
350
- result.update(binding=args.binding, process_id=backend.pid,
351
- minimized_before=backend.minimized_before,
352
- foreground_before=backend.foreground_before,
353
- foreground_after=backend.gui.GetForegroundWindow(),
354
- script_errors=backend.messages,
356
+ result.update(binding=args.binding, process_id=backend.pid, **backend.diagnostics(),
355
357
  input_method="Qt native methods; no keyboard/mouse input")
356
358
  output = json.dumps(result, ensure_ascii=False, indent=2)
357
359
  if args.report:
@@ -0,0 +1,145 @@
1
+ """Private, bounded-lifetime files for native workers; never use system TEMP."""
2
+ import json
3
+ import os
4
+ from pathlib import Path
5
+ import re
6
+ import shutil
7
+ import stat
8
+ import threading
9
+ import time
10
+ import uuid
11
+
12
+ import psutil
13
+
14
+
15
+ _SWEEP_LOCK = threading.Lock()
16
+ _LAST_SWEEP = {}
17
+ _WORKER_NAME = re.compile(r"worker-[0-9a-f]{32}\Z")
18
+
19
+
20
+ def worker_temp_root():
21
+ configured = os.environ.get("CFQUANT_FRIDA_TEMP_DIR")
22
+ if configured:
23
+ return Path(os.path.expandvars(configured)).expanduser().resolve()
24
+ runtime = os.environ.get("CFQUANT_RUNTIME_DIR")
25
+ if runtime:
26
+ return Path(os.path.expandvars(runtime)).expanduser().resolve() / "frida"
27
+ base = (os.environ.get("CFQUANT_HOME") or os.environ.get("CFQUANT_STATE_DIR")
28
+ or os.environ.get("CFQUANT_BASE_DIR") or str(Path(__file__).resolve().parents[1]))
29
+ return Path(os.path.expandvars(base)).expanduser().resolve() / "runtime" / "frida"
30
+
31
+
32
+ def _process_alive(pid, created):
33
+ try:
34
+ return abs(psutil.Process(int(pid)).create_time() - float(created)) < 0.01
35
+ except (psutil.NoSuchProcess, ValueError, TypeError):
36
+ return False
37
+ except psutil.AccessDenied:
38
+ return True
39
+
40
+
41
+ def _linked(path):
42
+ info = path.lstat()
43
+ return stat.S_ISLNK(info.st_mode) or bool(getattr(info, "st_file_attributes", 0)
44
+ & getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0x400))
45
+
46
+
47
+ def remove_worker_directory(root, directory):
48
+ """Delete only a direct owned child; never traverse a symlink or junction."""
49
+ root, directory = Path(root).resolve(), Path(directory)
50
+ try:
51
+ if (not _WORKER_NAME.fullmatch(directory.name) or _linked(directory)
52
+ or directory.resolve().parent != root):
53
+ return False
54
+ for parent, directories, files in os.walk(directory, followlinks=False):
55
+ if any(_linked(Path(parent) / name) for name in directories + files):
56
+ return False
57
+ # Keep the ownership marker until all possibly locked DLLs are gone.
58
+ # A partial rmtree must not make a crashed worker's files uncollectable.
59
+ marker = directory / "owner.json"
60
+ for child in directory.iterdir():
61
+ if child == marker:
62
+ continue
63
+ if child.is_dir():
64
+ shutil.rmtree(child)
65
+ else:
66
+ child.unlink()
67
+ marker.unlink(missing_ok=True)
68
+ directory.rmdir()
69
+ return True
70
+ except FileNotFoundError:
71
+ return True
72
+ except OSError:
73
+ return False # Loaded DLLs can remain locked briefly; a later sweep retries.
74
+
75
+
76
+ def sweep_worker_directories(root, *, force=False):
77
+ root = Path(root).resolve()
78
+ now = time.time()
79
+ with _SWEEP_LOCK:
80
+ if not force and now - _LAST_SWEEP.get(str(root), 0) < 60:
81
+ return
82
+ _LAST_SWEEP[str(root)] = now
83
+ if not root.is_dir():
84
+ return
85
+ for directory in root.iterdir():
86
+ try:
87
+ if (not _WORKER_NAME.fullmatch(directory.name) or _linked(directory)
88
+ or not directory.is_dir() or now - directory.stat().st_mtime < 60):
89
+ continue
90
+ marker = directory / "owner.json"
91
+ if _linked(marker):
92
+ continue
93
+ owner = json.loads(marker.read_text(encoding="utf-8"))
94
+ if owner.get("format") != "cfquant-native-worker-v1":
95
+ continue
96
+ if not isinstance(owner.get("owner_pid"), int) or not isinstance(owner.get("owner_created"), (int, float)):
97
+ continue
98
+ child = owner.get("child_pid")
99
+ if child is not None and (not isinstance(child, int) or not isinstance(owner.get("child_created"), (int, float))):
100
+ continue
101
+ # The parent's identity protects the mkdir/Popen registration window.
102
+ if child is None:
103
+ alive = _process_alive(owner.get("owner_pid"), owner.get("owner_created"))
104
+ else:
105
+ alive = _process_alive(child, owner.get("child_created"))
106
+ if not alive:
107
+ remove_worker_directory(root, directory)
108
+ except (OSError, ValueError, TypeError, AttributeError):
109
+ continue
110
+
111
+
112
+ class NativeWorkerFiles:
113
+ def __init__(self):
114
+ self.root = worker_temp_root()
115
+ self.root.mkdir(parents=True, exist_ok=True)
116
+ sweep_worker_directories(self.root)
117
+ self.path = self.root / ("worker-" + uuid.uuid4().hex)
118
+ self.path.mkdir()
119
+ self.owner = dict(format="cfquant-native-worker-v1", owner_pid=os.getpid(),
120
+ owner_created=psutil.Process().create_time())
121
+ try:
122
+ self._save()
123
+ except Exception:
124
+ self.close()
125
+ raise
126
+
127
+ def _save(self):
128
+ marker = self.path / "owner.json"
129
+ pending = marker.with_suffix(".pending")
130
+ pending.write_text(json.dumps(self.owner), encoding="utf-8")
131
+ pending.replace(marker)
132
+
133
+ def register(self, pid):
134
+ try:
135
+ created = psutil.Process(pid).create_time()
136
+ except psutil.NoSuchProcess:
137
+ created = 0 # The protocol reader reports an immediate child crash.
138
+ self.owner.update(child_pid=pid, child_created=created)
139
+ self._save()
140
+
141
+ def environment(self):
142
+ return {name: str(self.path) for name in ("TEMP", "TMP", "TMPDIR")}
143
+
144
+ def close(self):
145
+ return remove_worker_directory(self.root, self.path)
@@ -12,9 +12,11 @@ import sys
12
12
  import threading
13
13
  import time
14
14
 
15
+ from .qmt_native_files import NativeWorkerFiles, sweep_worker_directories
16
+
15
17
 
16
18
  LOGIN_METHODS = frozenset(("already_logged_in", "state", "select_account", "submit",
17
- "fill", "wait_outcome", "acknowledge_failure"))
19
+ "fill", "wait_outcome", "acknowledge_failure", "diagnostics"))
18
20
  STRATEGY_METHODS = frozenset(("snapshot", "start_strategy", "notices", "dismiss_notice"))
19
21
  MAX_MESSAGE = 4 * 1024 * 1024
20
22
 
@@ -40,14 +42,29 @@ class NativeWorker:
40
42
  self._sequence = 0
41
43
  self._closed = False
42
44
  self._tls_error = False
45
+ self.files = NativeWorkerFiles()
43
46
  env = dict(os.environ, PYTHONIOENCODING="utf-8", PYTHONDONTWRITEBYTECODE="1")
44
- self.process = subprocess.Popen(
45
- command or [sys.executable, "-u", "-m", "cfquant.qmt_native_worker"],
46
- stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
47
- text=True, encoding="utf-8", errors="replace", bufsize=1, env=env,
48
- cwd=str(Path(__file__).resolve().parents[1]),
49
- creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0) if os.name == "nt" else 0,
50
- )
47
+ env.update(self.files.environment())
48
+ try:
49
+ self.process = subprocess.Popen(
50
+ command or [sys.executable, "-u", "-m", "cfquant.qmt_native_worker"],
51
+ stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
52
+ text=True, encoding="utf-8", errors="replace", bufsize=1, env=env,
53
+ cwd=str(Path(__file__).resolve().parents[1]),
54
+ creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0) if os.name == "nt" else 0,
55
+ )
56
+ try:
57
+ self.files.register(self.process.pid)
58
+ except Exception:
59
+ if self.process.poll() is None:
60
+ self.process.kill()
61
+ self.process.wait(timeout=5)
62
+ for stream in (self.process.stdin, self.process.stdout, self.process.stderr):
63
+ stream.close()
64
+ raise
65
+ except Exception:
66
+ self.files.close()
67
+ raise
51
68
  self._readers = [
52
69
  threading.Thread(target=self._read_responses, name="qmt-native-response", daemon=True),
53
70
  threading.Thread(target=self._read_errors, name="qmt-native-stderr", daemon=True),
@@ -154,7 +171,7 @@ class NativeWorker:
154
171
  if not force and self.process.poll() is None:
155
172
  self.process.stdin.write(json.dumps(dict(id=0, method="close")) + "\n")
156
173
  self.process.stdin.flush()
157
- self.process.wait(timeout=1)
174
+ self.process.wait(timeout=3)
158
175
  except (OSError, ValueError, subprocess.TimeoutExpired):
159
176
  pass
160
177
  finally:
@@ -169,6 +186,79 @@ class NativeWorker:
169
186
  thread.join(timeout=1)
170
187
  self.process.stdout.close()
171
188
  self.process.stderr.close()
189
+ self.files.close()
190
+
191
+
192
+ class StrategyWorkerHost:
193
+ """Reuse one Frida host, but detach the QMT session after every inspection."""
194
+ def __init__(self, worker_factory=NativeWorker):
195
+ self.worker_factory = worker_factory
196
+ self.worker = None
197
+ self.lock = threading.RLock()
198
+ self.stopped = threading.Event()
199
+
200
+ def __call__(self, directory):
201
+ self.lock.acquire()
202
+ try:
203
+ if self.stopped.is_set():
204
+ raise QmtAutomationCancelled("QMT 策略检测已停止")
205
+ worker = self.worker
206
+ if worker is not None and (worker._closed or worker.process.poll() is not None):
207
+ worker.close(force=True)
208
+ self.worker = worker = None
209
+ if worker is None:
210
+ worker = self.worker_factory("strategy", directory, timeout=15,
211
+ should_cancel=self.stopped.is_set)
212
+ self.worker = worker
213
+ else:
214
+ sweep_worker_directories(worker.files.root)
215
+ info = worker.call("open", kind="strategy", directory=str(directory), timeout=15)
216
+ worker.pid, worker.main_hwnd = info.get("pid"), info.get("main_hwnd")
217
+ return _StrategyWorkerLease(self, worker)
218
+ except BaseException:
219
+ self._discard()
220
+ self.lock.release()
221
+ raise
222
+
223
+ def _discard(self):
224
+ if self.worker is not None:
225
+ worker, self.worker = self.worker, None
226
+ worker.close(force=True)
227
+
228
+ def close(self):
229
+ self.stopped.set()
230
+ with self.lock:
231
+ if self.worker is not None:
232
+ worker, self.worker = self.worker, None
233
+ worker.close()
234
+
235
+
236
+ class _StrategyWorkerLease:
237
+ def __init__(self, host, worker):
238
+ self.host, self.worker = host, worker
239
+ self.pid, self.main_hwnd = worker.pid, worker.main_hwnd
240
+ self.closed = False
241
+
242
+ def __getattr__(self, name):
243
+ if name not in STRATEGY_METHODS:
244
+ raise AttributeError(name)
245
+ def call(*args, **kwargs):
246
+ if self.closed:
247
+ raise QmtAutomationCancelled("QMT 策略检测会话已关闭")
248
+ return self.worker.call(name, *args, **kwargs)
249
+ return call
250
+
251
+ def close(self):
252
+ if self.closed:
253
+ return
254
+ self.closed = True
255
+ try:
256
+ try:
257
+ self.worker.call("release")
258
+ except Exception:
259
+ self.host._discard()
260
+ finally:
261
+ self.host.lock.release()
172
262
 
173
263
 
174
264
  class IsolatedLoginBackend(NativeWorker):
@@ -232,6 +322,13 @@ def serve(factory=_open_backend):
232
322
  opened = True
233
323
  methods = LOGIN_METHODS if kwargs["kind"] == "login" else STRATEGY_METHODS
234
324
  result = {key: getattr(backend, key, None) for key in ("pid", "main_hwnd")}
325
+ elif method == "release" and opened:
326
+ try:
327
+ if backend is not None:
328
+ backend.close()
329
+ finally:
330
+ backend, opened, methods = None, False, frozenset()
331
+ result = None
235
332
  elif opened and backend is not None and method in methods:
236
333
  result = getattr(backend, method)(*request.get("args", []), **request.get("kwargs", {}))
237
334
  else:
@@ -244,9 +341,17 @@ def serve(factory=_open_backend):
244
341
  output.flush()
245
342
  request = None
246
343
  finally:
247
- if backend is not None:
248
- backend.close()
249
- output.close()
344
+ try:
345
+ if backend is not None:
346
+ backend.close()
347
+ finally:
348
+ try:
349
+ # detach() releases a target session, not the host's DLL cache.
350
+ frida = sys.modules.get("frida")
351
+ if frida is not None:
352
+ frida.shutdown()
353
+ finally:
354
+ output.close()
250
355
 
251
356
 
252
357
  if __name__ == "__main__":
@@ -4,7 +4,7 @@
4
4
  // agent while the project is being upgraded.
5
5
  (() => {
6
6
  // Resolve query, start and notice APIs independently of the login agent.
7
- const missing = {gui: [], query: [], start: [], notice: [], dismiss: []};
7
+ const missing = {gui: [], query: [], start: [], whitelist: [], notice: [], dismiss: []};
8
8
  function native(group, moduleName, name, result, args) {
9
9
  const module = Process.findModuleByName(moduleName);
10
10
  const address = module && module.findExportByName(name);
@@ -30,7 +30,9 @@ const strategyManager = native('query', 'Formula.dll', '?instance@?$CSingletonT@
30
30
  const strategySave = native('query', 'Formula.dll', '?saveSettings@CStrategyTradeManager@TC@@QEAAXAEAVQDomElement@@AEAVQDomDocument@@@Z', 'void', ['pointer', 'pointer', 'pointer']);
31
31
  // isStrategyRun takes a request identifier; isFomrulaRun takes a formula name.
32
32
  const strategyRunning = native('query', 'Formula.dll', '?isFomrulaRun@CStrategyTradeManager@TC@@QEAA_NAEBVQString@@@Z', 'bool', ['pointer', 'pointer']);
33
- const strategyWhitelistReady = native('start', 'Formula.dll', '?isConnWhiteListReady@CStrategyTradeManager@TC@@QEBA_NXZ', 'bool', ['pointer']);
33
+ // Older QMT builds expose runStrategy without a separate readiness query.
34
+ // Use their native start path; never replace or change QMT whitelist checks.
35
+ const strategyWhitelistReady = native('whitelist', 'Formula.dll', '?isConnWhiteListReady@CStrategyTradeManager@TC@@QEBA_NXZ', 'bool', ['pointer']);
34
36
  const strategyRun = native('start', 'Formula.dll', '?runStrategy@CStrategyTradeManager@TC@@QEAAXHAEBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z', 'void', ['pointer', 'int', 'pointer']);
35
37
  const strategyDocCtor = native('query', 'Qt5Xml.dll', '??0QDomDocument@@QEAA@XZ', 'pointer', ['pointer']);
36
38
  const strategyDocDtor = native('query', 'Qt5Xml.dll', '??1QDomDocument@@QEAA@XZ', 'void', ['pointer']);
@@ -48,11 +50,12 @@ const closeNotice = native('dismiss', 'Dialog.dll', '?closeMessage@CGlobalMessag
48
50
  function capabilities() {
49
51
  const query = missing.gui.concat(missing.query);
50
52
  const start = query.concat(missing.start);
53
+ const whitelist = query.concat(missing.whitelist);
51
54
  const notice = query.concat(missing.notice);
52
55
  const dismiss = notice.concat(missing.dismiss);
53
- return {can_query: !query.length, can_start: !start.length,
56
+ return {can_query: !query.length, can_start: !start.length, can_check_whitelist: !whitelist.length,
54
57
  can_read_notices: !notice.length, can_dismiss_notices: !dismiss.length,
55
- missing_query: query, missing_start: start, missing_notice: notice, missing_dismiss: dismiss};
58
+ missing_query: query, missing_start: start, missing_whitelist: whitelist, missing_notice: notice, missing_dismiss: dismiss};
56
59
  }
57
60
  const typeNames = new Map();
58
61
  function hasType(object, name) {
@@ -228,7 +231,7 @@ rpc.exports.strategystart = (hwnd, expected) => onGui(hwnd, () => {
228
231
  throw new Error('Not an eligible managed strategy');
229
232
  }
230
233
  if (model.running) return {outcome: 'already_running', requested: false};
231
- if (!snapshot.whitelist_ready) return {outcome: 'waiting_whitelist', requested: false};
234
+ if (snapshot.whitelist_ready === false) return {outcome: 'waiting_whitelist', requested: false};
232
235
  // Empty release-mode MSVC x64 std::string (SSO), matching the exported ABI.
233
236
  const reason = Memory.alloc(32);
234
237
  reason.writeByteArray(new Uint8Array(32));
@@ -848,6 +848,39 @@ class QmtStrategyManager:
848
848
  if changed:
849
849
  self._save()
850
850
 
851
+ def prepare_for_start(self, directory):
852
+ """Finish this terminal's offline deployment before allowing launch."""
853
+ root = qmt_root(directory)
854
+ with self.lock:
855
+ if self.load_error:
856
+ raise ValueError(self.load_error)
857
+ jobs = [job for job in self.jobs.values()
858
+ if os.path.normcase(str(qmt_root(job["root"]))) == os.path.normcase(str(root))]
859
+ if not jobs:
860
+ return
861
+ if self.process_checker(root):
862
+ raise RuntimeError("QMT 尚未完全退出,无法完成启动前策略部署")
863
+ try:
864
+ for job in jobs:
865
+ if job.get("enabled") and self._prepare_generation_transition(job):
866
+ self._control(job)
867
+ try:
868
+ self._advance(job)
869
+ except Exception as error:
870
+ count = int(job.get("deploy_retry_count") or 0) + 1
871
+ job["deploy_retry_count"] = count
872
+ job["deploy_retry_after"] = time.time() + min(10.0, 2.0 * count)
873
+ self._state(job, "error", str(error))
874
+ raise
875
+ if (job.get("state") not in ("configured", "waiting_import", "disabled")
876
+ or (job.get("enabled") and any(
877
+ not role.get("model_prepared") or role.get("reimport_required")
878
+ for role in job["roles"]))):
879
+ raise RuntimeError("QMT 启动前策略部署尚未完成: " +
880
+ (job.get("message") or job.get("state", "")))
881
+ finally:
882
+ self._save()
883
+
851
884
  def _advance(self, job):
852
885
  root = qmt_root(job["root"])
853
886
  if self.process_checker(root):