cfquant 0.2.61__tar.gz → 0.2.62__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 (240) hide show
  1. {cfquant-0.2.61 → cfquant-0.2.62}/PKG-INFO +1 -1
  2. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/qmt_component_login.py +16 -7
  3. cfquant-0.2.62/cfquant/qmt_strategy_agent.js +239 -0
  4. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/qmt_strategy_recovery.py +24 -25
  5. cfquant-0.2.62/cfquant/tests/test_qmt_strategy_capabilities.py +189 -0
  6. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_qmt_strategy_recovery.py +59 -4
  7. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/version.py +2 -2
  8. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant.egg-info/PKG-INFO +1 -1
  9. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant.egg-info/SOURCES.txt +1 -0
  10. {cfquant-0.2.61 → cfquant-0.2.62}/pyproject.toml +1 -1
  11. {cfquant-0.2.61 → cfquant-0.2.62}/qmt_scripts/CFQUANT_LITE.py +1 -1
  12. {cfquant-0.2.61 → cfquant-0.2.62}/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
  13. {cfquant-0.2.61 → cfquant-0.2.62}/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
  14. {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/app.js +1 -1
  15. {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/index.html +2 -2
  16. cfquant-0.2.61/cfquant/qmt_strategy_agent.js +0 -131
  17. {cfquant-0.2.61 → cfquant-0.2.62}/LICENSE +0 -0
  18. {cfquant-0.2.61 → cfquant-0.2.62}/LTtx/__init__.py +0 -0
  19. {cfquant-0.2.61 → cfquant-0.2.62}/LTtx/tx/Config.txt +0 -0
  20. {cfquant-0.2.61 → cfquant-0.2.62}/LTtx/tx/LTtx_server.py +0 -0
  21. {cfquant-0.2.61 → cfquant-0.2.62}/LTtx/tx/__init__.py +0 -0
  22. {cfquant-0.2.61 → cfquant-0.2.62}/LTtx/tx/tx.py +0 -0
  23. {cfquant-0.2.61 → cfquant-0.2.62}/MANIFEST.in +0 -0
  24. {cfquant-0.2.61 → cfquant-0.2.62}/README.md +0 -0
  25. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/__init__.py +0 -0
  26. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/_editable_install.py +0 -0
  27. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/account_routing.py +0 -0
  28. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/algo.py +0 -0
  29. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/algo_broker.py +0 -0
  30. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/batch_orders.py +0 -0
  31. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/build_info.py +0 -0
  32. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/callback_dispatcher.py +0 -0
  33. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/cftrader.py +0 -0
  34. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/channels.py +0 -0
  35. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/cli.py +0 -0
  36. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/client.py +0 -0
  37. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/config.py +0 -0
  38. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/diagnostics.py +0 -0
  39. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/flow_control.py +0 -0
  40. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/hub.py +0 -0
  41. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/level2.py +0 -0
  42. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/log_management.py +0 -0
  43. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/logging_i18n.py +0 -0
  44. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/management.py +0 -0
  45. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/normal_bridge.py +0 -0
  46. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/order_identity.py +0 -0
  47. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/order_meta.py +0 -0
  48. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/pipe_bridge.py +0 -0
  49. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/pipe_client.py +0 -0
  50. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/pipe_hub.py +0 -0
  51. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/pipe_transport.py +0 -0
  52. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/protocol.py +0 -0
  53. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/qmt_binding_defaults.py +0 -0
  54. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/qmt_binding_settings.py +0 -0
  55. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/qmt_bridge.py +0 -0
  56. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/qmt_component_agent.js +0 -0
  57. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/qmt_credentials.py +0 -0
  58. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/qmt_dependencies.py +0 -0
  59. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/qmt_native_worker.py +0 -0
  60. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/qmt_pipe_runtime.py +0 -0
  61. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/qmt_strategy_deploy.py +0 -0
  62. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/qmt_strategy_package.py +0 -0
  63. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/qmt_strategy_runtime.py +0 -0
  64. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/qmt_update.py +0 -0
  65. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/quote_history.py +0 -0
  66. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/quote_leases.py +0 -0
  67. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/quote_service.py +0 -0
  68. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/remote_adapter.py +0 -0
  69. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/remote_management.py +0 -0
  70. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/remote_supervisor.py +0 -0
  71. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/rpc_timing.py +0 -0
  72. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/runtime_report.py +0 -0
  73. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/shared_quotes.py +0 -0
  74. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/stock_connect.py +0 -0
  75. {cfquant-0.2.61 → cfquant-0.2.62}/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
  76. {cfquant-0.2.61 → cfquant-0.2.62}/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
  77. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/12_/350/256/242/345/215/225/345/244/207/346/263/250/346/265/213/350/257/225.py" +0 -0
  78. {cfquant-0.2.61 → cfquant-0.2.62}/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
  79. {cfquant-0.2.61 → cfquant-0.2.62}/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
  80. {cfquant-0.2.61 → cfquant-0.2.62}/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
  81. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/16_/345/220/257/345/212/250/345/256/211/350/243/205/346/265/213/350/257/225.py" +0 -0
  82. {cfquant-0.2.61 → cfquant-0.2.62}/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
  83. {cfquant-0.2.61 → cfquant-0.2.62}/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
  84. {cfquant-0.2.61 → cfquant-0.2.62}/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
  85. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/1_/350/241/214/346/203/205/346/216/245/346/224/266/346/265/213/350/257/225.py" +0 -0
  86. {cfquant-0.2.61 → cfquant-0.2.62}/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
  87. {cfquant-0.2.61 → cfquant-0.2.62}/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
  88. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/22_/350/277/236/346/216/245/350/257/212/346/226/255/346/265/213/350/257/225.py" +0 -0
  89. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/23_/347/244/272/344/276/213/351/205/215/347/275/256/346/265/213/350/257/225.py" +0 -0
  90. {cfquant-0.2.61 → cfquant-0.2.62}/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
  91. {cfquant-0.2.61 → cfquant-0.2.62}/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
  92. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/26_/345/205/250/346/216/250/350/256/242/351/230/205/346/265/213/350/257/225.py" +0 -0
  93. {cfquant-0.2.61 → cfquant-0.2.62}/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
  94. {cfquant-0.2.61 → cfquant-0.2.62}/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
  95. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/29_Level2/350/241/214/346/203/205/346/265/213/350/257/225.py" +0 -0
  96. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/2_/346/225/260/346/215/256/350/216/267/345/217/226/346/265/213/350/257/225.py" +0 -0
  97. {cfquant-0.2.61 → cfquant-0.2.62}/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
  98. {cfquant-0.2.61 → cfquant-0.2.62}/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
  99. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/32_/345/274/202/346/255/245/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  100. {cfquant-0.2.61 → cfquant-0.2.62}/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
  101. {cfquant-0.2.61 → cfquant-0.2.62}/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
  102. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/3_/346/225/260/346/215/256/344/270/213/350/275/275/346/265/213/350/257/225.py" +0 -0
  103. {cfquant-0.2.61 → cfquant-0.2.62}/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
  104. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/5_/347/234/237/345/256/236/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  105. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/6_/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
  106. {cfquant-0.2.61 → cfquant-0.2.62}/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
  107. {cfquant-0.2.61 → cfquant-0.2.62}/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
  108. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/9_/345/217/257/346/222/244/350/256/242/345/215/225/346/265/213/350/257/225.py" +0 -0
  109. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/_helpers.py +0 -0
  110. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/readme.md +0 -0
  111. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_account_query_timing.py +0 -0
  112. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_account_subscription_probe.py +0 -0
  113. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_advanced_parity.py +0 -0
  114. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_algo.py +0 -0
  115. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_algo_broker.py +0 -0
  116. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_api_tester_browser.py +0 -0
  117. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_api_tester_web.py +0 -0
  118. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
  119. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_binding_live_refresh_browser.py +0 -0
  120. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_binding_runtime_browser.py +0 -0
  121. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_callback_recovery.py +0 -0
  122. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_callback_requests.py +0 -0
  123. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_cftrader.py +0 -0
  124. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_cftrader_tutorial.py +0 -0
  125. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_cftrader_web.py +0 -0
  126. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_credit_compat.py +0 -0
  127. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_dataframe_encoding.py +0 -0
  128. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_json_serialization.py +0 -0
  129. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_latency_flow_control.py +0 -0
  130. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_lite_account_routing.py +0 -0
  131. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_lite_deployment_paths.py +0 -0
  132. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_lite_parity.py +0 -0
  133. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_lite_qmt_dispatch.py +0 -0
  134. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_local_trading_calendar.py +0 -0
  135. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_log_management.py +0 -0
  136. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_management.py +0 -0
  137. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_management_batch.py +0 -0
  138. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_market_data_fallback.py +0 -0
  139. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_market_timing.py +0 -0
  140. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_order_identity.py +0 -0
  141. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_order_time_callback.py +0 -0
  142. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_pipe_shutdown.py +0 -0
  143. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_project_updater.py +0 -0
  144. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_python_api_reference.py +0 -0
  145. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_qmt_auto_login_browser.py +0 -0
  146. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_qmt_auto_login_integration.py +0 -0
  147. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_qmt_binding_defaults.py +0 -0
  148. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_qmt_binding_settings.py +0 -0
  149. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_qmt_component_login.py +0 -0
  150. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_qmt_dependencies.py +0 -0
  151. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_qmt_fileless.py +0 -0
  152. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_qmt_native_worker.py +0 -0
  153. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_qmt_strategy_browser.py +0 -0
  154. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_qmt_strategy_deploy.py +0 -0
  155. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_qmt_strategy_web.py +0 -0
  156. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_qmt_update.py +0 -0
  157. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_quote_latency.py +0 -0
  158. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_quote_latency_encoding.py +0 -0
  159. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_quote_service.py +0 -0
  160. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_remote_management.py +0 -0
  161. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_remote_management_browser.py +0 -0
  162. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_remote_version_check.py +0 -0
  163. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
  164. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_shared_quotes.py +0 -0
  165. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_shared_quotes_generated.py +0 -0
  166. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_startup_recovery.py +0 -0
  167. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_stock_connect.py +0 -0
  168. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_stock_connect_browser.py +0 -0
  169. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_subscription_congestion.py +0 -0
  170. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_table_conversion.py +0 -0
  171. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_trade_strategy_callback.py +0 -0
  172. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_trader_subscription_recovery.py +0 -0
  173. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_trading_dates.py +0 -0
  174. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_trading_monitor.py +0 -0
  175. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_trading_monitor_browser.py +0 -0
  176. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_tutorial_reader.py +0 -0
  177. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_tx_auto_import.py +0 -0
  178. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tests/test_tx_shutdown.py +0 -0
  179. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/trading_calendar.py +0 -0
  180. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/trading_monitor.py +0 -0
  181. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tx.py +0 -0
  182. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/tx_trade_bridge.py +0 -0
  183. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/xtconstant.py +0 -0
  184. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/xtdata.py +0 -0
  185. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/xttrader.py +0 -0
  186. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant/xttype.py +0 -0
  187. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant.egg-info/dependency_links.txt +0 -0
  188. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant.egg-info/entry_points.txt +0 -0
  189. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant.egg-info/requires.txt +0 -0
  190. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant.egg-info/top_level.txt +0 -0
  191. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant_pipe_hub.py +0 -0
  192. {cfquant-0.2.61 → cfquant-0.2.62}/cfquant_web_server.py +0 -0
  193. {cfquant-0.2.61 → cfquant-0.2.62}/qmt_scripts/CFQUANT.py +0 -0
  194. {cfquant-0.2.61 → cfquant-0.2.62}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +0 -0
  195. {cfquant-0.2.61 → cfquant-0.2.62}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
  196. {cfquant-0.2.61 → cfquant-0.2.62}/qmt_scripts/__init__.py +0 -0
  197. {cfquant-0.2.61 → cfquant-0.2.62}/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
  198. {cfquant-0.2.61 → cfquant-0.2.62}/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
  199. {cfquant-0.2.61 → cfquant-0.2.62}/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
  200. {cfquant-0.2.61 → cfquant-0.2.62}/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
  201. {cfquant-0.2.61 → cfquant-0.2.62}/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
  202. {cfquant-0.2.61 → cfquant-0.2.62}/requirements.txt +0 -0
  203. {cfquant-0.2.61 → cfquant-0.2.62}/setup.cfg +0 -0
  204. {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/__init__.py +0 -0
  205. {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/api-tester.js +0 -0
  206. {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/avatars/amber-pulse.svg +0 -0
  207. {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/avatars/copper-grid.svg +0 -0
  208. {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/avatars/market-blue.svg +0 -0
  209. {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/avatars/rose-circuit.svg +0 -0
  210. {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/avatars/signal-green.svg +0 -0
  211. {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/avatars/slate-wave.svg +0 -0
  212. {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/avatars/teal-orbit.svg +0 -0
  213. {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/avatars/violet-node.svg +0 -0
  214. {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/cftrader-api-data.js +0 -0
  215. {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/docs.js +0 -0
  216. {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/favicon.svg +0 -0
  217. {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
  218. {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
  219. {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
  220. {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
  221. {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
  222. {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
  223. {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
  224. {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
  225. {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
  226. {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
  227. {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
  228. {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
  229. {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
  230. {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
  231. {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
  232. {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
  233. {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
  234. {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
  235. {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/python-api-data.js +0 -0
  236. {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/python-api.js +0 -0
  237. {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/remote-management.js +0 -0
  238. {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/styles.css +0 -0
  239. {cfquant-0.2.61 → cfquant-0.2.62}/web_dashboard/trading-monitor.js +0 -0
  240. {cfquant-0.2.61 → cfquant-0.2.62}/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.62
4
4
  Summary: QMT local bridge and xtquant-compatible Python client.
5
5
  License: MIT
6
6
  Requires-Python: >=3.8
@@ -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()
@@ -0,0 +1,239 @@
1
+ 'use strict';
2
+
3
+ // A scope also permits an older Python host to append this agent to its login
4
+ // agent while the project is being upgraded.
5
+ (() => {
6
+ // Resolve query, start and notice APIs independently of the login agent.
7
+ const missing = {gui: [], query: [], start: [], notice: [], dismiss: []};
8
+ function native(group, moduleName, name, result, args) {
9
+ const module = Process.findModuleByName(moduleName);
10
+ const address = module && module.findExportByName(name);
11
+ if (!address) {
12
+ missing[group].push(moduleName + ':' + name);
13
+ return null;
14
+ }
15
+ return new NativeFunction(address, result, args);
16
+ }
17
+ const findWidget = native('gui', 'Qt5Widgets.dll', '?find@QWidget@@SAPEAV1@_K@Z', 'pointer', ['uint64']);
18
+ const inherits = native('gui', 'Qt5Core.dll', '?inherits@QObject@@QEBA_NPEBD@Z', 'bool', ['pointer', 'pointer']);
19
+ const getThread = native('gui', 'user32.dll', 'GetWindowThreadProcessId', 'uint32', ['pointer', 'pointer']);
20
+ const postMessage = native('gui', 'user32.dll', 'PostMessageW', 'bool', ['pointer', 'uint32', 'pointer', 'pointer']);
21
+ const dispatchModule = Process.findModuleByName('user32.dll');
22
+ const dispatchAddress = dispatchModule && dispatchModule.findExportByName('DispatchMessageW');
23
+ if (!dispatchAddress) missing.gui.push('user32.dll:DispatchMessageW');
24
+ if (Process.arch !== 'x64' || Process.pointerSize !== 8) missing.gui.push('x64 ABI');
25
+ const stringCtor = native('query', 'Qt5Core.dll', '??0QString@@QEAA@PEBVQChar@@H@Z', 'pointer', ['pointer', 'pointer', 'int']);
26
+ const stringDtor = native('query', 'Qt5Core.dll', '??1QString@@QEAA@XZ', 'void', ['pointer']);
27
+ const stringSize = native('query', 'Qt5Core.dll', '?size@QString@@QEBAHXZ', 'int', ['pointer']);
28
+ const stringData = native('query', 'Qt5Core.dll', '?utf16@QString@@QEBAPEBGXZ', 'pointer', ['pointer']);
29
+ const strategyManager = native('query', 'Formula.dll', '?instance@?$CSingletonT@VCStrategyTradeManager@TC@@@utils@@SAPEAVCStrategyTradeManager@TC@@XZ', 'pointer', []);
30
+ const strategySave = native('query', 'Formula.dll', '?saveSettings@CStrategyTradeManager@TC@@QEAAXAEAVQDomElement@@AEAVQDomDocument@@@Z', 'void', ['pointer', 'pointer', 'pointer']);
31
+ // isStrategyRun takes a request identifier; isFomrulaRun takes a formula name.
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']);
34
+ 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
+ const strategyDocCtor = native('query', 'Qt5Xml.dll', '??0QDomDocument@@QEAA@XZ', 'pointer', ['pointer']);
36
+ const strategyDocDtor = native('query', 'Qt5Xml.dll', '??1QDomDocument@@QEAA@XZ', 'void', ['pointer']);
37
+ const strategyElemDtor = native('query', 'Qt5Xml.dll', '??1QDomElement@@QEAA@XZ', 'void', ['pointer']);
38
+ const strategyCreateElem = native('query', 'Qt5Xml.dll', '?createElement@QDomDocument@@QEAA?AVQDomElement@@AEBVQString@@@Z', 'pointer', ['pointer', 'pointer', 'pointer']);
39
+ const strategyFirst = native('query', 'Qt5Xml.dll', '?firstChildElement@QDomNode@@QEBA?AVQDomElement@@AEBVQString@@@Z', 'pointer', ['pointer', 'pointer', 'pointer']);
40
+ const strategyNext = native('query', 'Qt5Xml.dll', '?nextSiblingElement@QDomNode@@QEBA?AVQDomElement@@AEBVQString@@@Z', 'pointer', ['pointer', 'pointer', 'pointer']);
41
+ const strategyNull = native('query', 'Qt5Xml.dll', '?isNull@QDomNode@@QEBA_NXZ', 'bool', ['pointer']);
42
+ const strategyAttr = native('query', 'Qt5Xml.dll', '?attribute@QDomElement@@QEBA?AVQString@@AEBV2@0@Z', 'pointer', ['pointer', 'pointer', 'pointer', 'pointer']);
43
+ const isVisible = native('notice', 'Qt5Widgets.dll', '?isVisible@QWidget@@QEBA_NXZ', 'bool', ['pointer']);
44
+ const children = native('notice', 'Qt5Core.dll', '?children@QObject@@QEBAAEBV?$QList@PEAVQObject@@@@XZ', 'pointer', ['pointer']);
45
+ const noticeText = native('notice', 'BaseWidget.dll', '?getText@CSimpleTextWidget@XP@@QEBA?AVQString@@XZ', 'pointer', ['pointer', 'pointer']);
46
+ const closeNotice = native('dismiss', 'Dialog.dll', '?closeMessage@CGlobalMessageDialog@XP@@SAXXZ', 'void', []);
47
+
48
+ function capabilities() {
49
+ const query = missing.gui.concat(missing.query);
50
+ const start = query.concat(missing.start);
51
+ const notice = query.concat(missing.notice);
52
+ const dismiss = notice.concat(missing.dismiss);
53
+ return {can_query: !query.length, can_start: !start.length,
54
+ can_read_notices: !notice.length, can_dismiss_notices: !dismiss.length,
55
+ missing_query: query, missing_start: start, missing_notice: notice, missing_dismiss: dismiss};
56
+ }
57
+ const typeNames = new Map();
58
+ function hasType(object, name) {
59
+ if (!typeNames.has(name)) typeNames.set(name, Memory.allocUtf8String(name));
60
+ return inherits(object, typeNames.get(name));
61
+ }
62
+ function getRoot(hwnd) {
63
+ const root = findWidget(uint64(hwnd));
64
+ if (root.isNull()) throw new Error('No Qt widget for target window');
65
+ return root;
66
+ }
67
+ let pending = null;
68
+ function onGui(hwnd, operation) {
69
+ if (missing.gui.length) throw new Error('Missing Qt GUI interfaces: ' + missing.gui.join(', '));
70
+ if (pending) throw new Error('An operation is already pending');
71
+ const thread = getThread(ptr(hwnd), NULL);
72
+ if (!thread) throw new Error('Target window no longer exists');
73
+ return new Promise((resolve, reject) => {
74
+ const timer = setTimeout(() => {
75
+ if (pending && pending.resolve === resolve) {
76
+ pending = null;
77
+ reject(new Error('GUI operation timeout'));
78
+ }
79
+ }, 8000);
80
+ pending = {thread, operation, resolve, reject, timer};
81
+ if (!postMessage(ptr(hwnd), 0, NULL, NULL)) {
82
+ pending = null;
83
+ clearTimeout(timer);
84
+ reject(new Error('Cannot wake QMT GUI thread'));
85
+ }
86
+ });
87
+ }
88
+ if (!missing.gui.length) Interceptor.attach(dispatchAddress, {
89
+ onLeave() {
90
+ if (!pending || this.threadId !== pending.thread) return;
91
+ const job = pending;
92
+ pending = null;
93
+ clearTimeout(job.timer);
94
+ try { job.resolve({gui_thread: this.threadId, result: job.operation()}); }
95
+ catch (error) { job.reject(error); }
96
+ }
97
+ });
98
+
99
+ function strategyString(text, action) {
100
+ const value = Memory.alloc(Process.pointerSize);
101
+ stringCtor(value, Memory.allocUtf16String(text), text.length);
102
+ try { return action(value); } finally { stringDtor(value); }
103
+ }
104
+
105
+ function readString(value) {
106
+ const size = stringSize(value);
107
+ if (size < 0 || size > 65536) throw new Error('Invalid Qt string size');
108
+ return size ? stringData(value).readUtf16String(size) : '';
109
+ }
110
+
111
+ function strategyAttribute(element, name) {
112
+ return strategyString(name, key => strategyString('', empty => {
113
+ const value = Memory.alloc(Process.pointerSize);
114
+ strategyAttr(element, value, key, empty);
115
+ try { return readString(value); }
116
+ finally { stringDtor(value); }
117
+ }));
118
+ }
119
+
120
+ function strategySnapshot(root, checkStartup = false) {
121
+ const support = capabilities();
122
+ if (!support.can_query) throw new Error('Missing strategy query interfaces: ' + support.missing_query.join(', '));
123
+ if (!hasType(root, 'TC::CMainWindow')) throw new Error('QMT is not logged in');
124
+ const manager = strategyManager();
125
+ if (manager.isNull()) throw new Error('Strategy manager unavailable');
126
+ const doc = Memory.alloc(64), parent = Memory.alloc(64), models = [];
127
+ strategyDocCtor(doc);
128
+ try {
129
+ strategyString('Models', tag => strategyCreateElem(doc, parent, tag));
130
+ try {
131
+ // Serializes only into our temporary DOM; no configuration is written.
132
+ strategySave(manager, parent, doc);
133
+ strategyString('item', tag => {
134
+ let element = Memory.alloc(64);
135
+ strategyFirst(parent, element, tag);
136
+ try {
137
+ while (!strategyNull(element)) {
138
+ if (models.length >= 1000) throw new Error('Too many QMT models');
139
+ const row = {};
140
+ for (const key of ['id', 'name', 'account', 'm_strAccountKey', 'runMode',
141
+ 'startupAutorun', 'eStrategyType', 'stock', 'peroid']) {
142
+ row[key] = strategyAttribute(element, key);
143
+ }
144
+ row.running = Boolean(strategyString(row.name, name => strategyRunning(manager, name)));
145
+ models.push(row);
146
+ const next = Memory.alloc(64);
147
+ strategyNext(element, next, tag);
148
+ strategyElemDtor(element);
149
+ element = next;
150
+ }
151
+ } finally { strategyElemDtor(element); }
152
+ });
153
+ } finally { strategyElemDtor(parent); }
154
+ } finally { strategyDocDtor(doc); }
155
+ return {models, capabilities: support,
156
+ whitelist_ready: checkStartup && strategyWhitelistReady ? Boolean(strategyWhitelistReady(manager)) : null};
157
+ }
158
+
159
+ rpc.exports.strategycapabilities = capabilities;
160
+ rpc.exports.status = hwnd => onGui(hwnd, () => {
161
+ const root = getRoot(hwnd);
162
+ return {is_main: hasType(root, 'TC::CMainWindow'), is_login: hasType(root, 'TC::CLoginDialog'), login_identity: null};
163
+ });
164
+ rpc.exports.strategystatus = hwnd => onGui(hwnd, () => strategySnapshot(getRoot(hwnd)));
165
+ function childObjects(object) {
166
+ // Qt 5 x64 QList<QObject*>; never assume broker-specific widget offsets.
167
+ const data = children(object).readPointer();
168
+ const begin = data.add(8).readS32(), end = data.add(12).readS32();
169
+ if (begin < 0 || end < begin || end > 5000) throw new Error('Unexpected QList layout');
170
+ const result = [];
171
+ for (let index = begin; index < end; index++) result.push(data.add(16 + index * Process.pointerSize).readPointer());
172
+ return result;
173
+ }
174
+ function illegalIpNotice(hwnd) {
175
+ if (!capabilities().can_read_notices) return null;
176
+ const root = getRoot(hwnd);
177
+ if (!(hasType(root, 'XP::CGlobalMessageDialog') || hasType(root, 'CGlobalMessageDialog')) || !isVisible(root)) return null;
178
+ const queue = [root], seen = new Set(), matches = [];
179
+ while (queue.length) {
180
+ const object = queue.shift();
181
+ if (object.isNull() || seen.has(object.toString())) continue;
182
+ seen.add(object.toString());
183
+ if (seen.size > 512) throw new Error('Message widget tree too large');
184
+ if ((hasType(object, 'XP::CSimpleTextWidget') || hasType(object, 'CSimpleTextWidget')) && isVisible(object)) {
185
+ const value = Memory.alloc(Process.pointerSize);
186
+ noticeText(object, value);
187
+ let text;
188
+ try { text = readString(value); } finally { stringDtor(value); }
189
+ if (/^\[防外接\]\s*Python进程访问了网络地址\s*\[[^\]]+\]\s*[,,]\s*策略运行中止[!!]\s*$/.test(text)) matches.push(text);
190
+ }
191
+ queue.push(...childObjects(object));
192
+ }
193
+ return matches.length === 1 ? {hwnd, text: matches[0]} : null;
194
+ }
195
+ rpc.exports.illegalipnotice = hwnd => onGui(hwnd, () => illegalIpNotice(hwnd));
196
+ rpc.exports.dismissillegalip = (mainHwnd, hwnd, text, expected) => onGui(mainHwnd, () => {
197
+ if (!capabilities().can_dismiss_notices) return {outcome: 'notice_close_unavailable', closed: false};
198
+ const models = strategySnapshot(getRoot(mainHwnd)).models;
199
+ for (const target of expected) {
200
+ const matches = models.filter(model => model.name === target.name);
201
+ if (matches.length !== 1 || Object.keys(target).some(key => matches[0][key] !== target[key])) {
202
+ throw new Error('Strategy binding changed before warning dismissal');
203
+ }
204
+ }
205
+ if (!expected.length) throw new Error('No bound strategy');
206
+ const current = illegalIpNotice(hwnd);
207
+ if (!current) return {outcome: 'already_hidden_or_changed', closed: false};
208
+ if (current.text !== text) return {outcome: 'message_changed', closed: false};
209
+ closeNotice();
210
+ const closed = !isVisible(getRoot(hwnd));
211
+ return {outcome: closed ? 'closed' : 'close_failed', closed};
212
+ });
213
+ rpc.exports.strategystart = (hwnd, expected) => onGui(hwnd, () => {
214
+ if (!capabilities().can_start) return {outcome: 'detection_only', requested: false};
215
+ // Re-read on the same GUI turn as the mutation. Cached IDs are insufficient.
216
+ const snapshot = strategySnapshot(getRoot(hwnd), true);
217
+ const matches = snapshot.models.filter(row => row.name === expected.name);
218
+ if (matches.length !== 1) throw new Error('No unique bound strategy');
219
+ const model = matches[0];
220
+ for (const key of ['id', 'name', 'account', 'm_strAccountKey', 'runMode',
221
+ 'startupAutorun', 'eStrategyType', 'stock', 'peroid']) {
222
+ if (typeof expected[key] !== 'string' || !expected[key] || model[key] !== expected[key]) {
223
+ throw new Error('Strategy binding changed: ' + key);
224
+ }
225
+ }
226
+ if (!/^CFQ_[A-Za-z0-9_]+$/.test(model.name) || model.startupAutorun !== '1' ||
227
+ model.eStrategyType !== '4' || !/^[1-9][0-9]*$/.test(model.id) || Number(model.id) > 2147483647) {
228
+ throw new Error('Not an eligible managed strategy');
229
+ }
230
+ if (model.running) return {outcome: 'already_running', requested: false};
231
+ if (!snapshot.whitelist_ready) return {outcome: 'waiting_whitelist', requested: false};
232
+ // Empty release-mode MSVC x64 std::string (SSO), matching the exported ABI.
233
+ const reason = Memory.alloc(32);
234
+ reason.writeByteArray(new Uint8Array(32));
235
+ reason.add(24).writeU64(15);
236
+ strategyRun(strategyManager(), Number(model.id), reason);
237
+ return {outcome: 'start_requested', requested: true};
238
+ });
239
+ })();
@@ -6,7 +6,6 @@ The command line defaults to inspection; --ensure/--watch enable recovery.
6
6
  import argparse
7
7
  import copy
8
8
  import datetime
9
- import hashlib
10
9
  import json
11
10
  import os
12
11
  from pathlib import Path
@@ -24,17 +23,6 @@ from .qmt_strategy_deploy import (
24
23
  )
25
24
 
26
25
 
27
- # Verified on Guojin simulation 2.1.33.0 / Qt 5.6.2 x64. Native ABI support is
28
- # deliberately explicit; another build needs its own inspection and live test.
29
- SUPPORTED_FORMULA_SHA256 = {
30
- "673e512bab175e068cbee5fa349e33025bf564e5f57192ff3509f87dad8b8214",
31
- }
32
- NOTICE_DLL_SHA256 = {
33
- "Dialog.dll": "b71c3dc77cfa830e1037ac2b7f1cc10a43aabec113a9a4c77d0fa97b34cedd67",
34
- "BaseWidget.dll": "31fd0313f8b2b1f55f0d367121782ec3dc941a884d59a40cfc77ae155e2e861b",
35
- }
36
-
37
-
38
26
  def visible_notice_windows(pid, main_hwnd):
39
27
  if os.name != "nt" or not pid or not main_hwnd:
40
28
  return []
@@ -92,18 +80,14 @@ class UnsupportedQmt(RuntimeError):
92
80
  class QtStrategyBackend(QtLoginBackend):
93
81
  def __init__(self, root):
94
82
  directory = qmt_root(root) / "bin.x64"
95
- digest = hashlib.sha256((directory / "Formula.dll").read_bytes()).hexdigest()
96
- if digest not in SUPPORTED_FORMULA_SHA256:
97
- raise UnsupportedQmt("QMT 策略组件版本尚未验证,未执行自动启动")
98
- self.notice_supported = all(hashlib.sha256((directory / name).read_bytes()).hexdigest() == digest
99
- for name, digest in NOTICE_DLL_SHA256.items())
100
83
  extension = Path(__file__).with_name("qmt_strategy_agent.js").read_text(encoding="utf-8")
101
- super().__init__(directory, agent_extension=extension)
84
+ super().__init__(directory, agent_extension=extension, strategy=True)
102
85
  try:
103
86
  if not self.already_logged_in():
104
87
  raise QmtNotReady("QMT 尚未登录,等待主界面")
105
- if "strategystatus" not in self.script.list_exports_sync():
106
- raise UnsupportedQmt("QMT 策略组件接口不可用")
88
+ self.capabilities = self.api.strategycapabilities()
89
+ if not self.capabilities.get("can_query"):
90
+ raise UnsupportedQmt("QMT 缺少策略运行检测接口: " + ", ".join(self.capabilities.get("missing_query", [])))
107
91
  except Exception:
108
92
  self.close()
109
93
  raise
@@ -115,7 +99,7 @@ class QtStrategyBackend(QtLoginBackend):
115
99
  return self.api.strategystart(str(self.main_hwnd), expected)["result"]
116
100
 
117
101
  def notices(self):
118
- if not self.notice_supported:
102
+ if not self.capabilities.get("can_read_notices"):
119
103
  return []
120
104
  result = []
121
105
  for hwnd in visible_notice_windows(self.pid, self.main_hwnd):
@@ -186,7 +170,15 @@ class QmtStrategyRecovery:
186
170
  event_id=(status.get("incident") or {}).get("id"), **details))
187
171
 
188
172
  def _notices(self, job, status, backend, expected):
189
- for notice in backend.notices():
173
+ try:
174
+ notices = backend.notices()
175
+ status.pop("notice_error", None)
176
+ except Exception as error:
177
+ # An optional broker-specific message widget must not disable
178
+ # the independently supported strategy-state query.
179
+ status["notice_error"] = str(error)
180
+ return
181
+ for notice in notices:
190
182
  if expected_models(job, self.accounts_provider()) != expected or not self.job_is_current(job):
191
183
  raise ValueError("Binding changed before warning dismissal")
192
184
  incident = status.get("incident") or {}
@@ -249,7 +241,9 @@ class QmtStrategyRecovery:
249
241
  if (status.get("pid"), status.get("generation")) != (backend.pid, job["generation"]):
250
242
  status = {"last_checked_at": now, "next_check_at": now + self.interval,
251
243
  "pid": backend.pid, "generation": job["generation"], "attempts": {}}
252
- models = match_models(backend.snapshot(), expected)
244
+ snapshot = backend.snapshot()
245
+ models = match_models(snapshot, expected)
246
+ status["capabilities"] = snapshot.get("capabilities") or {}
253
247
  status["main_hwnd"] = getattr(backend, "main_hwnd", None)
254
248
  if not inspect_only:
255
249
  self._notices(job, status, backend, expected)
@@ -271,7 +265,9 @@ class QmtStrategyRecovery:
271
265
  else:
272
266
  status.pop("stable_since", None)
273
267
  status["state"] = "stopped"
274
- if not inspect_only:
268
+ if not inspect_only and status["capabilities"].get("can_start") is False:
269
+ status["state"] = "detection_only"
270
+ elif not inspect_only:
275
271
  attempts = dict(status.get("attempts") or {})
276
272
  status["attempts"] = attempts
277
273
  if any(attempts.get(target["name"], 0) >= 5 for target in stopped):
@@ -389,8 +385,11 @@ class QmtStrategyRecoveryScheduler:
389
385
  "backoff": "托管策略尚未恢复,等待冷却后重试",
390
386
  "retry_exhausted": "托管策略连续启动失败,已暂停重试,请检查 QMT 提示",
391
387
  }[state]
388
+ elif state == "detection_only":
389
+ target["state"] = "waiting_manual_start"
390
+ target["message"] = "已检测到托管策略停止,当前 QMT 缺少自动恢复接口,请手动启动策略"
392
391
  elif state == "unsupported" and target.get("state") != "running":
393
- target["message"] = recovery.get("error", "当前 QMT 版本尚未验证策略自动恢复")
392
+ target["message"] = recovery.get("error", "当前 QMT 缺少策略运行检测接口")
394
393
  return status
395
394
 
396
395
  def start(self):
@@ -0,0 +1,189 @@
1
+ """Run the actual Frida agent against synthetic Qt exports; no live QMT."""
2
+ import json
3
+ from pathlib import Path
4
+ import shutil
5
+ import subprocess
6
+ from types import SimpleNamespace
7
+
8
+ import pytest
9
+
10
+ from cfquant.qmt_component_login import _verify_environment_native
11
+
12
+
13
+ AGENT = Path(__file__).resolve().parents[1] / 'qmt_strategy_agent.js'
14
+ HARNESS = r'''
15
+ const fs = require('fs'), vm = require('vm');
16
+ const options = JSON.parse(fs.readFileSync(0, 'utf8'));
17
+ let nextId = 1, hook, starts = 0, closes = 0, whitelistChecks = 0;
18
+ class Pointer {
19
+ constructor(value = {}, offset = 0, id = nextId++) { this.value = value; this.offset = offset; this.id = id; }
20
+ add(offset) { return new Pointer(this.value, this.offset + offset, this.id); }
21
+ isNull() { return this.value === null; }
22
+ toString() { return String(this.id); }
23
+ readPointer() {
24
+ if (this.value.list) return this.value.list;
25
+ return this.value.items[(this.offset - 16) / 8];
26
+ }
27
+ readS32() { return this.offset === 8 ? 0 : this.value.items.length; }
28
+ readUtf16String() { return this.value.text; }
29
+ writeByteArray() {}
30
+ writeU64() {}
31
+ }
32
+ const target = {id:'7', name:'CFQ_TEST001_LITE', account:'TEST001', m_strAccountKey:'key',
33
+ runMode:'1', startupAutorun:'1', eStrategyType:'4', stock:'SH000300', peroid:'86400'};
34
+ const models = [Object.assign({}, target, {running: options.running !== false})];
35
+ const main = new Pointer({types:['TC::CMainWindow'], children:[]});
36
+ const text = '[防外接] Python进程访问了网络地址 [192.0.2.1],策略运行中止!';
37
+ const content = new Pointer({types:[options.unqualified ? 'CSimpleTextWidget' : 'XP::CSimpleTextWidget'], text, children:[]});
38
+ const container = new Pointer({types:['QWidget'], children:[content]});
39
+ const notice = new Pointer({types:['XP::CGlobalMessageDialog'], children:[container]});
40
+ if (options.duplicate) notice.value.children.push(new Pointer({types:['XP::CSimpleTextWidget'], text, children:[]}));
41
+ if (options.unrelated) content.value.text = 'An unrelated message';
42
+ const manager = new Pointer({});
43
+ function call(symbol, args) {
44
+ const [a,b,c] = args;
45
+ if (symbol === 'DispatchMessageW') return;
46
+ if (symbol === 'GetWindowThreadProcessId') return 1;
47
+ if (symbol === 'PostMessageW') { setImmediate(() => hook.onLeave.call({threadId:1})); return true; }
48
+ if (symbol.startsWith('?find@QWidget')) return String(a) === '1' ? main : notice;
49
+ if (symbol.startsWith('?inherits@')) return a.value.types.includes(b.value.text);
50
+ if (symbol.startsWith('?isVisible@')) return a.value.visible !== false;
51
+ if (symbol.startsWith('??0QString')) { a.value.text = b.value.text; return a; }
52
+ if (symbol.startsWith('??1QString')) return;
53
+ if (symbol.startsWith('?size@QString')) return a.value.text.length;
54
+ if (symbol.startsWith('?utf16@QString')) return a;
55
+ if (symbol.startsWith('?instance@')) return manager;
56
+ if (symbol.startsWith('?saveSettings@')) { b.value.models = models; return; }
57
+ if (symbol.startsWith('?isFomrulaRun@')) return models.some(row => row.name === b.value.text && row.running);
58
+ if (symbol.startsWith('?isConnWhiteListReady@')) { whitelistChecks++; return options.whitelist !== false; }
59
+ if (symbol.startsWith('?runStrategy@')) { starts++; return; }
60
+ if (symbol.startsWith('??0QDomDocument') || symbol.startsWith('??1QDomDocument') || symbol.startsWith('??1QDomElement')) return;
61
+ if (symbol.startsWith('?createElement@')) return b;
62
+ if (symbol.startsWith('?firstChildElement@')) { b.value.models = a.value.models; b.value.index = 0; return b; }
63
+ if (symbol.startsWith('?nextSiblingElement@')) { b.value.models = a.value.models; b.value.index = a.value.index + 1; return b; }
64
+ if (symbol.startsWith('?isNull@QDomNode')) return a.value.index >= a.value.models.length;
65
+ if (symbol.startsWith('?attribute@')) { b.value.text = a.value.models[a.value.index][c.value.text]; return b; }
66
+ if (symbol.startsWith('?children@')) return new Pointer({list:new Pointer({items:a.value.children})});
67
+ if (symbol.startsWith('?getText@')) { b.value.text = a.value.text; return b; }
68
+ if (symbol.startsWith('?closeMessage@')) { closes++; notice.value.visible = false; return; }
69
+ throw new Error('Unimplemented test native call: ' + symbol);
70
+ }
71
+ const context = {
72
+ Process: {arch:'x64', pointerSize:8, findModuleByName: name => {
73
+ if ((options.missingModules || []).includes(name)) return null;
74
+ return {findExportByName: symbol => (options.missing || []).some(prefix => symbol.startsWith(prefix)) ? null : symbol};
75
+ }},
76
+ NativeFunction: function(symbol) { return (...args) => call(symbol, args); },
77
+ Memory: {alloc: () => new Pointer(), allocUtf8String: text => new Pointer({text}), allocUtf16String: text => new Pointer({text})},
78
+ Interceptor: {attach: (address, callbacks) => { hook = callbacks; }},
79
+ rpc: {exports:{}}, uint64: value => value, ptr: value => value, NULL:null, setTimeout, clearTimeout
80
+ };
81
+ vm.createContext(context);
82
+ vm.runInContext(fs.readFileSync(options.agent, 'utf8'), context);
83
+ (async () => {
84
+ const api = context.rpc.exports, result = {capabilities:api.strategycapabilities()};
85
+ if (result.capabilities.can_query) {
86
+ result.snapshot = (await api.strategystatus('1')).result;
87
+ if (options.tryStart) result.start = (await api.strategystart('1', Object.assign({}, target, options.expected || {}))).result;
88
+ if (options.notice) {
89
+ result.notice = (await api.illegalipnotice('2')).result;
90
+ if (result.notice) {
91
+ if (options.changed) content.value.text = text.replace('192.0.2.1','192.0.2.2');
92
+ result.dismiss = (await api.dismissillegalip('1','2',result.notice.text,[target])).result;
93
+ }
94
+ }
95
+ } else {
96
+ try { await api.strategystatus('1'); } catch (error) { result.query_error = error.message; }
97
+ }
98
+ result.starts = starts; result.closes = closes; result.whitelistChecks = whitelistChecks;
99
+ process.stdout.write(JSON.stringify(result));
100
+ })().catch(error => { process.stdout.write(JSON.stringify({error:error.message,starts,closes})); });
101
+ '''
102
+
103
+
104
+ def run_agent(**options):
105
+ node = shutil.which('node')
106
+ assert node, 'Node is required to validate the actual strategy agent'
107
+ result = subprocess.run([node, '-e', HARNESS], input=json.dumps(dict(options, agent=str(AGENT))),
108
+ text=True, encoding='utf-8', capture_output=True, check=True, timeout=15)
109
+ return json.loads(result.stdout)
110
+
111
+
112
+ @pytest.mark.parametrize('missing', [['?isConnWhiteListReady@'], ['?runStrategy@'], ['?getText@'], ['?closeMessage@']])
113
+ def test_optional_export_does_not_prevent_query(missing):
114
+ result = run_agent(missing=missing, tryStart=True, running=False)
115
+ assert result['snapshot']['models'][0]['running'] is False
116
+ assert result['capabilities']['can_query']
117
+ if missing[0] in ('?isConnWhiteListReady@', '?runStrategy@'):
118
+ assert not result['capabilities']['can_start']
119
+ assert result['start'] == {'outcome': 'detection_only', 'requested': False}
120
+ assert result['starts'] == 0
121
+ if missing[0] == '?isConnWhiteListReady@':
122
+ assert result['snapshot']['whitelist_ready'] is None
123
+
124
+
125
+ @pytest.mark.parametrize('module', ['LoginDialog.dll', 'Dialog.dll', 'BaseWidget.dll'])
126
+ def test_query_does_not_depend_on_login_or_notice_modules(module):
127
+ result = run_agent(missingModules=[module])
128
+ assert result['snapshot']['models'][0]['running'] is True
129
+ assert result['whitelistChecks'] == 0
130
+
131
+
132
+ def test_missing_core_query_interface_is_explicit():
133
+ result = run_agent(missing=['?isFomrulaRun@'])
134
+ assert not result['capabilities']['can_query']
135
+ assert 'isFomrulaRun' in result['query_error']
136
+ assert result['starts'] == 0
137
+
138
+
139
+ @pytest.mark.parametrize('running,whitelist,outcome,starts', [
140
+ (True, True, 'already_running', 0), (False, False, 'waiting_whitelist', 0),
141
+ (False, True, 'start_requested', 1),
142
+ ])
143
+ def test_supported_start_keeps_native_guards(running, whitelist, outcome, starts):
144
+ result = run_agent(tryStart=True, running=running, whitelist=whitelist)
145
+ assert result['start']['outcome'] == outcome and result['starts'] == starts
146
+
147
+
148
+ def test_start_rejects_changed_binding():
149
+ result = run_agent(tryStart=True, running=False, expected={'account':'WRONG'})
150
+ assert 'binding changed' in result['error'] and result['starts'] == 0
151
+
152
+
153
+ @pytest.mark.parametrize('options,outcome', [({}, 'closed'), ({'unqualified':True}, 'closed'), ({'changed':True}, 'message_changed'),
154
+ ({'missing':['?closeMessage@']}, 'notice_close_unavailable')])
155
+ def test_notice_uses_nested_widget_types_and_rechecks_text(options, outcome):
156
+ result = run_agent(notice=True, **options)
157
+ assert result['notice']['hwnd'] == '2'
158
+ assert result['dismiss']['outcome'] == outcome
159
+ assert result['closes'] == (1 if outcome == 'closed' else 0)
160
+
161
+
162
+ @pytest.mark.parametrize('options', [{'duplicate':True}, {'unrelated':True}, {'missingModules':['BaseWidget.dll']}])
163
+ def test_ambiguous_unrelated_or_unsupported_notice_is_not_closed(options):
164
+ result = run_agent(notice=True, **options)
165
+ assert result['notice'] is None and result['closes'] == 0
166
+ assert result['snapshot']['models'][0]['running'] is True
167
+
168
+
169
+ @pytest.mark.parametrize('qt_version', [(5,6,2), (5,9,9), (5,15,2), (6,5,0)])
170
+ def test_strategy_environment_checks_qt_abi_without_exact_version_pin(tmp_path, monkeypatch, qt_version):
171
+ from cfquant import qmt_dependencies
172
+ data = bytearray(128)
173
+ data[0x3c:0x40] = (64).to_bytes(4, 'little')
174
+ data[68:70] = (0x8664).to_bytes(2, 'little')
175
+ (tmp_path / 'XtItClient.exe').write_bytes(data)
176
+ major, minor, patch = qt_version
177
+ api = SimpleNamespace(GetFileVersionInfo=lambda *args: {'FileVersionMS': (major << 16) | minor, 'FileVersionLS': patch << 16})
178
+ monkeypatch.setattr(qmt_dependencies, 'import_login_module', lambda name: api)
179
+ if major == 5:
180
+ _verify_environment_native(tmp_path, strategy=True)
181
+ else:
182
+ with pytest.raises(RuntimeError, match='ABI'):
183
+ _verify_environment_native(tmp_path, strategy=True)
184
+ # Login retains its existing independently validated compatibility policy.
185
+ if qt_version == (5,6,2):
186
+ _verify_environment_native(tmp_path)
187
+ else:
188
+ with pytest.raises(RuntimeError, match='5.6.2'):
189
+ _verify_environment_native(tmp_path)