cfquant 0.2.61__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 (244) hide show
  1. {cfquant-0.2.61 → cfquant-0.2.63}/PKG-INFO +3 -1
  2. {cfquant-0.2.61 → cfquant-0.2.63}/README.md +2 -0
  3. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/management.py +32 -0
  4. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/qmt_component_login.py +24 -13
  5. cfquant-0.2.63/cfquant/qmt_native_files.py +145 -0
  6. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/qmt_native_worker.py +117 -12
  7. cfquant-0.2.63/cfquant/qmt_strategy_agent.js +242 -0
  8. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/qmt_strategy_deploy.py +33 -0
  9. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/qmt_strategy_recovery.py +139 -67
  10. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_qmt_auto_login_integration.py +24 -1
  11. {cfquant-0.2.61 → 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.61 → cfquant-0.2.63}/cfquant/tests/test_qmt_native_worker.py +7 -0
  15. cfquant-0.2.63/cfquant/tests/test_qmt_strategy_capabilities.py +209 -0
  16. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_qmt_strategy_deploy.py +70 -0
  17. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_qmt_strategy_recovery.py +59 -4
  18. cfquant-0.2.63/cfquant/tests/test_strategy_recovery_api.py +239 -0
  19. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/version.py +2 -2
  20. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant.egg-info/PKG-INFO +3 -1
  21. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant.egg-info/SOURCES.txt +5 -0
  22. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant_web_server.py +42 -3
  23. {cfquant-0.2.61 → cfquant-0.2.63}/pyproject.toml +1 -1
  24. {cfquant-0.2.61 → cfquant-0.2.63}/qmt_scripts/CFQUANT_LITE.py +1 -1
  25. {cfquant-0.2.61 → 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
  26. {cfquant-0.2.61 → 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
  27. {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/app.js +1 -1
  28. {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/index.html +2 -2
  29. cfquant-0.2.61/cfquant/qmt_strategy_agent.js +0 -131
  30. {cfquant-0.2.61 → cfquant-0.2.63}/LICENSE +0 -0
  31. {cfquant-0.2.61 → cfquant-0.2.63}/LTtx/__init__.py +0 -0
  32. {cfquant-0.2.61 → cfquant-0.2.63}/LTtx/tx/Config.txt +0 -0
  33. {cfquant-0.2.61 → cfquant-0.2.63}/LTtx/tx/LTtx_server.py +0 -0
  34. {cfquant-0.2.61 → cfquant-0.2.63}/LTtx/tx/__init__.py +0 -0
  35. {cfquant-0.2.61 → cfquant-0.2.63}/LTtx/tx/tx.py +0 -0
  36. {cfquant-0.2.61 → cfquant-0.2.63}/MANIFEST.in +0 -0
  37. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/__init__.py +0 -0
  38. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/_editable_install.py +0 -0
  39. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/account_routing.py +0 -0
  40. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/algo.py +0 -0
  41. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/algo_broker.py +0 -0
  42. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/batch_orders.py +0 -0
  43. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/build_info.py +0 -0
  44. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/callback_dispatcher.py +0 -0
  45. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/cftrader.py +0 -0
  46. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/channels.py +0 -0
  47. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/cli.py +0 -0
  48. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/client.py +0 -0
  49. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/config.py +0 -0
  50. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/diagnostics.py +0 -0
  51. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/flow_control.py +0 -0
  52. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/hub.py +0 -0
  53. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/level2.py +0 -0
  54. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/log_management.py +0 -0
  55. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/logging_i18n.py +0 -0
  56. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/normal_bridge.py +0 -0
  57. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/order_identity.py +0 -0
  58. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/order_meta.py +0 -0
  59. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/pipe_bridge.py +0 -0
  60. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/pipe_client.py +0 -0
  61. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/pipe_hub.py +0 -0
  62. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/pipe_transport.py +0 -0
  63. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/protocol.py +0 -0
  64. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/qmt_binding_defaults.py +0 -0
  65. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/qmt_binding_settings.py +0 -0
  66. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/qmt_bridge.py +0 -0
  67. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/qmt_component_agent.js +0 -0
  68. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/qmt_credentials.py +0 -0
  69. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/qmt_dependencies.py +0 -0
  70. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/qmt_pipe_runtime.py +0 -0
  71. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/qmt_strategy_package.py +0 -0
  72. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/qmt_strategy_runtime.py +0 -0
  73. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/qmt_update.py +0 -0
  74. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/quote_history.py +0 -0
  75. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/quote_leases.py +0 -0
  76. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/quote_service.py +0 -0
  77. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/remote_adapter.py +0 -0
  78. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/remote_management.py +0 -0
  79. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/remote_supervisor.py +0 -0
  80. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/rpc_timing.py +0 -0
  81. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/runtime_report.py +0 -0
  82. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/shared_quotes.py +0 -0
  83. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/stock_connect.py +0 -0
  84. {cfquant-0.2.61 → 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
  85. {cfquant-0.2.61 → 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
  86. {cfquant-0.2.61 → 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
  87. {cfquant-0.2.61 → 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
  88. {cfquant-0.2.61 → 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
  89. {cfquant-0.2.61 → 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
  90. {cfquant-0.2.61 → 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
  91. {cfquant-0.2.61 → 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
  92. {cfquant-0.2.61 → 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
  93. {cfquant-0.2.61 → 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
  94. {cfquant-0.2.61 → 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
  95. {cfquant-0.2.61 → 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
  96. {cfquant-0.2.61 → 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
  97. {cfquant-0.2.61 → 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
  98. {cfquant-0.2.61 → 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
  99. {cfquant-0.2.61 → 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
  100. {cfquant-0.2.61 → 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
  101. {cfquant-0.2.61 → 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
  102. {cfquant-0.2.61 → 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
  103. {cfquant-0.2.61 → 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
  104. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/29_Level2/350/241/214/346/203/205/346/265/213/350/257/225.py" +0 -0
  105. {cfquant-0.2.61 → 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
  106. {cfquant-0.2.61 → 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
  107. {cfquant-0.2.61 → 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
  108. {cfquant-0.2.61 → 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
  109. {cfquant-0.2.61 → 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
  110. {cfquant-0.2.61 → 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
  111. {cfquant-0.2.61 → 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
  112. {cfquant-0.2.61 → 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
  113. {cfquant-0.2.61 → 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
  114. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/6_/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
  115. {cfquant-0.2.61 → 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
  116. {cfquant-0.2.61 → 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
  117. {cfquant-0.2.61 → 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
  118. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/_helpers.py +0 -0
  119. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/readme.md +0 -0
  120. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_account_query_timing.py +0 -0
  121. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_account_subscription_probe.py +0 -0
  122. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_advanced_parity.py +0 -0
  123. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_algo.py +0 -0
  124. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_algo_broker.py +0 -0
  125. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_api_tester_browser.py +0 -0
  126. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_api_tester_web.py +0 -0
  127. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
  128. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_binding_live_refresh_browser.py +0 -0
  129. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_binding_runtime_browser.py +0 -0
  130. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_callback_recovery.py +0 -0
  131. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_callback_requests.py +0 -0
  132. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_cftrader.py +0 -0
  133. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_cftrader_tutorial.py +0 -0
  134. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_cftrader_web.py +0 -0
  135. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_credit_compat.py +0 -0
  136. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_dataframe_encoding.py +0 -0
  137. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_json_serialization.py +0 -0
  138. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_latency_flow_control.py +0 -0
  139. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_lite_account_routing.py +0 -0
  140. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_lite_deployment_paths.py +0 -0
  141. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_lite_parity.py +0 -0
  142. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_lite_qmt_dispatch.py +0 -0
  143. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_local_trading_calendar.py +0 -0
  144. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_log_management.py +0 -0
  145. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_management.py +0 -0
  146. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_management_batch.py +0 -0
  147. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_market_data_fallback.py +0 -0
  148. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_market_timing.py +0 -0
  149. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_order_identity.py +0 -0
  150. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_order_time_callback.py +0 -0
  151. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_pipe_shutdown.py +0 -0
  152. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_project_updater.py +0 -0
  153. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_python_api_reference.py +0 -0
  154. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_qmt_auto_login_browser.py +0 -0
  155. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_qmt_binding_defaults.py +0 -0
  156. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_qmt_binding_settings.py +0 -0
  157. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_qmt_dependencies.py +0 -0
  158. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_qmt_fileless.py +0 -0
  159. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_qmt_strategy_browser.py +0 -0
  160. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_qmt_strategy_web.py +0 -0
  161. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_qmt_update.py +0 -0
  162. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_quote_latency.py +0 -0
  163. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_quote_latency_encoding.py +0 -0
  164. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_quote_service.py +0 -0
  165. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_remote_management.py +0 -0
  166. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_remote_management_browser.py +0 -0
  167. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_remote_version_check.py +0 -0
  168. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
  169. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_shared_quotes.py +0 -0
  170. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_shared_quotes_generated.py +0 -0
  171. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_startup_recovery.py +0 -0
  172. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_stock_connect.py +0 -0
  173. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_stock_connect_browser.py +0 -0
  174. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_subscription_congestion.py +0 -0
  175. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_table_conversion.py +0 -0
  176. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_trade_strategy_callback.py +0 -0
  177. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_trader_subscription_recovery.py +0 -0
  178. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_trading_dates.py +0 -0
  179. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_trading_monitor.py +0 -0
  180. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_trading_monitor_browser.py +0 -0
  181. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_tutorial_reader.py +0 -0
  182. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_tx_auto_import.py +0 -0
  183. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tests/test_tx_shutdown.py +0 -0
  184. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/trading_calendar.py +0 -0
  185. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/trading_monitor.py +0 -0
  186. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tx.py +0 -0
  187. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/tx_trade_bridge.py +0 -0
  188. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/xtconstant.py +0 -0
  189. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/xtdata.py +0 -0
  190. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/xttrader.py +0 -0
  191. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant/xttype.py +0 -0
  192. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant.egg-info/dependency_links.txt +0 -0
  193. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant.egg-info/entry_points.txt +0 -0
  194. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant.egg-info/requires.txt +0 -0
  195. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant.egg-info/top_level.txt +0 -0
  196. {cfquant-0.2.61 → cfquant-0.2.63}/cfquant_pipe_hub.py +0 -0
  197. {cfquant-0.2.61 → cfquant-0.2.63}/qmt_scripts/CFQUANT.py +0 -0
  198. {cfquant-0.2.61 → cfquant-0.2.63}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +0 -0
  199. {cfquant-0.2.61 → cfquant-0.2.63}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
  200. {cfquant-0.2.61 → cfquant-0.2.63}/qmt_scripts/__init__.py +0 -0
  201. {cfquant-0.2.61 → 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
  202. {cfquant-0.2.61 → 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
  203. {cfquant-0.2.61 → 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
  204. {cfquant-0.2.61 → 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
  205. {cfquant-0.2.61 → 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
  206. {cfquant-0.2.61 → cfquant-0.2.63}/requirements.txt +0 -0
  207. {cfquant-0.2.61 → cfquant-0.2.63}/setup.cfg +0 -0
  208. {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/__init__.py +0 -0
  209. {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/api-tester.js +0 -0
  210. {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/avatars/amber-pulse.svg +0 -0
  211. {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/avatars/copper-grid.svg +0 -0
  212. {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/avatars/market-blue.svg +0 -0
  213. {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/avatars/rose-circuit.svg +0 -0
  214. {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/avatars/signal-green.svg +0 -0
  215. {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/avatars/slate-wave.svg +0 -0
  216. {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/avatars/teal-orbit.svg +0 -0
  217. {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/avatars/violet-node.svg +0 -0
  218. {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/cftrader-api-data.js +0 -0
  219. {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/docs.js +0 -0
  220. {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/favicon.svg +0 -0
  221. {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
  222. {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
  223. {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
  224. {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
  225. {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
  226. {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
  227. {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
  228. {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
  229. {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
  230. {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
  231. {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
  232. {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
  233. {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
  234. {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
  235. {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
  236. {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
  237. {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
  238. {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
  239. {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/python-api-data.js +0 -0
  240. {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/python-api.js +0 -0
  241. {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/remote-management.js +0 -0
  242. {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/styles.css +0 -0
  243. {cfquant-0.2.61 → cfquant-0.2.63}/web_dashboard/trading-monitor.js +0 -0
  244. {cfquant-0.2.61 → 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.61
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):
@@ -27,7 +27,7 @@ def verify_environment(directory):
27
27
  verify_environment_isolated(directory)
28
28
 
29
29
 
30
- def _verify_environment_native(directory):
30
+ def _verify_environment_native(directory, *, strategy=False):
31
31
  """Only invoked inside the native worker or the standalone login CLI."""
32
32
  try:
33
33
  from .qmt_dependencies import LOGIN_MODULES, import_login_module
@@ -38,14 +38,17 @@ def _verify_environment_native(directory):
38
38
  directory = Path(directory)
39
39
  for module in ("Qt5Core.dll", "Qt5Widgets.dll"):
40
40
  version = win32api.GetFileVersionInfo(str(directory / module), "\\")
41
- if (version["FileVersionMS"], version["FileVersionLS"]) != ((5 << 16) | 6, 2 << 16):
41
+ if strategy:
42
+ if version["FileVersionMS"] >> 16 != 5:
43
+ raise RuntimeError("QMT 策略检测需要兼容的 Qt 5 接口,当前组件 ABI 不受支持")
44
+ elif (version["FileVersionMS"], version["FileVersionLS"]) != ((5 << 16) | 6, 2 << 16):
42
45
  raise RuntimeError("QMT 组件自动登录当前仅支持 Qt 5.6.2,请先适配此客户端版本")
43
46
  with (directory / "XtItClient.exe").open("rb") as stream:
44
47
  stream.seek(0x3C)
45
48
  offset = int.from_bytes(stream.read(4), "little")
46
49
  stream.seek(offset + 4)
47
50
  if int.from_bytes(stream.read(2), "little") != 0x8664:
48
- raise RuntimeError("QMT 组件自动登录需要 64 位客户端")
51
+ raise RuntimeError("QMT 原生组件操作需要 64 位客户端")
49
52
 
50
53
 
51
54
  def auto_login(backend, password_provider, *, retry_not_ready=False, login_account=None,
@@ -132,7 +135,7 @@ def select_binding(config_path, name):
132
135
 
133
136
 
134
137
  class QtLoginBackend:
135
- def __init__(self, directory, timeout=40, expected_pid=None, *, agent_extension=""):
138
+ def __init__(self, directory, timeout=40, expected_pid=None, *, agent_extension="", strategy=False):
136
139
  try:
137
140
  from .qmt_dependencies import import_login_module
138
141
  except ImportError:
@@ -170,7 +173,10 @@ class QtLoginBackend:
170
173
  self.pid = matches[0].pid
171
174
  if expected_pid is not None and self.pid != expected_pid:
172
175
  raise RuntimeError("QMT process changed; stopping this login attempt")
173
- _verify_environment_native(directory)
176
+ if strategy:
177
+ _verify_environment_native(directory, strategy=True)
178
+ else:
179
+ _verify_environment_native(directory)
174
180
  self.mutex = win32event.CreateMutex(None, False, "Local\\CFQuantQmtComponentLogin_" + str(self.pid))
175
181
  acquired = win32event.WaitForSingleObject(self.mutex, 0)
176
182
  if acquired not in (0, 0x80):
@@ -179,13 +185,16 @@ class QtLoginBackend:
179
185
  raise RuntimeError("Another component login is already running for this QMT")
180
186
  try:
181
187
  self.session = frida.attach(self.pid)
182
- readiness = self.session.create_script("rpc.exports.ready = () => ['Qt5Core.dll', 'Qt5Widgets.dll', 'LoginDialog.dll'].every(n => Process.findModuleByName(n) !== null);")
188
+ modules = ["Qt5Core.dll", "Qt5Widgets.dll"]
189
+ if not strategy:
190
+ modules.append("LoginDialog.dll")
191
+ readiness = self.session.create_script("rpc.exports.ready = () => %s.every(n => Process.findModuleByName(n) !== null);" % json.dumps(modules))
183
192
  readiness.load()
184
193
  ready = readiness.exports_sync.ready()
185
194
  readiness.unload()
186
195
  if not ready:
187
196
  raise QmtNotReady("QMT login modules are still loading")
188
- source = Path(__file__).with_name("qmt_component_agent.js").read_text(encoding="utf-8")
197
+ source = "" if strategy else Path(__file__).with_name("qmt_component_agent.js").read_text(encoding="utf-8")
189
198
  script = self.session.create_script(source + "\n" + agent_extension)
190
199
  script.on("message", lambda message, data: self.messages.append("agent_error") if message.get("type") == "error" else None)
191
200
  script.load()
@@ -301,6 +310,12 @@ class QtLoginBackend:
301
310
  self.api.acknowledge(str(self.failure_hwnd))
302
311
  self.failure_hwnd = None
303
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
+
304
319
  def close(self):
305
320
  try:
306
321
  from .qmt_dependencies import import_login_module
@@ -332,17 +347,13 @@ def main():
332
347
  if not 1 <= args.timeout <= 120:
333
348
  parser.error("timeout must be between 1 and 120 seconds")
334
349
  row, directory = select_binding(args.config, args.binding)
335
- backend = QtLoginBackend(directory, args.timeout)
350
+ backend = IsolatedLoginBackend(directory, args.timeout)
336
351
  try:
337
352
  backend.select_account((row.get("qmt_auto_login") or {}).get("login_account", ""), replace=True)
338
353
  def read_password():
339
354
  return getpass.getpass("QMT password (hidden): ") if args.prompt else sys.stdin.readline().rstrip("\r\n")
340
355
  result = auto_login(backend, read_password)
341
- result.update(binding=args.binding, process_id=backend.pid,
342
- minimized_before=backend.minimized_before,
343
- foreground_before=backend.foreground_before,
344
- foreground_after=backend.gui.GetForegroundWindow(),
345
- script_errors=backend.messages,
356
+ result.update(binding=args.binding, process_id=backend.pid, **backend.diagnostics(),
346
357
  input_method="Qt native methods; no keyboard/mouse input")
347
358
  output = json.dumps(result, ensure_ascii=False, indent=2)
348
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__":