cfquant 0.2.59__tar.gz → 0.2.60__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 (237) hide show
  1. {cfquant-0.2.59 → cfquant-0.2.60}/PKG-INFO +2 -2
  2. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/_editable_install.py +30 -4
  3. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/normal_bridge.py +1 -1
  4. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/protocol.py +2 -1
  5. cfquant-0.2.60/cfquant/qmt_binding_defaults.py +54 -0
  6. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/qmt_component_login.py +14 -2
  7. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/qmt_dependencies.py +35 -3
  8. cfquant-0.2.60/cfquant/qmt_native_worker.py +253 -0
  9. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/qmt_strategy_recovery.py +7 -2
  10. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/rpc_timing.py +2 -0
  11. cfquant-0.2.60/cfquant/tests/test_account_subscription_probe.py +233 -0
  12. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_lite_account_routing.py +4 -2
  13. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_qmt_auto_login_browser.py +31 -0
  14. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_qmt_auto_login_integration.py +30 -6
  15. cfquant-0.2.60/cfquant/tests/test_qmt_binding_defaults.py +116 -0
  16. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_qmt_component_login.py +6 -6
  17. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_qmt_dependencies.py +68 -0
  18. cfquant-0.2.60/cfquant/tests/test_qmt_native_worker.py +166 -0
  19. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_qmt_strategy_browser.py +1 -1
  20. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_qmt_strategy_recovery.py +20 -0
  21. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_qmt_strategy_web.py +3 -3
  22. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_subscription_congestion.py +7 -6
  23. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_trader_subscription_recovery.py +21 -6
  24. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tx_trade_bridge.py +33 -2
  25. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/version.py +2 -2
  26. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/xttrader.py +43 -7
  27. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant.egg-info/PKG-INFO +2 -2
  28. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant.egg-info/SOURCES.txt +5 -0
  29. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant.egg-info/requires.txt +1 -1
  30. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant_web_server.py +92 -14
  31. {cfquant-0.2.59 → cfquant-0.2.60}/pyproject.toml +2 -2
  32. {cfquant-0.2.59 → cfquant-0.2.60}/qmt_scripts/CFQUANT_LITE.py +37 -5
  33. {cfquant-0.2.59 → cfquant-0.2.60}/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 +37 -5
  34. {cfquant-0.2.59 → cfquant-0.2.60}/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 +37 -5
  35. {cfquant-0.2.59 → cfquant-0.2.60}/requirements.txt +1 -1
  36. {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/app.js +5 -5
  37. {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/index.html +8 -8
  38. {cfquant-0.2.59 → cfquant-0.2.60}/LICENSE +0 -0
  39. {cfquant-0.2.59 → cfquant-0.2.60}/LTtx/__init__.py +0 -0
  40. {cfquant-0.2.59 → cfquant-0.2.60}/LTtx/tx/Config.txt +0 -0
  41. {cfquant-0.2.59 → cfquant-0.2.60}/LTtx/tx/LTtx_server.py +0 -0
  42. {cfquant-0.2.59 → cfquant-0.2.60}/LTtx/tx/__init__.py +0 -0
  43. {cfquant-0.2.59 → cfquant-0.2.60}/LTtx/tx/tx.py +0 -0
  44. {cfquant-0.2.59 → cfquant-0.2.60}/MANIFEST.in +0 -0
  45. {cfquant-0.2.59 → cfquant-0.2.60}/README.md +0 -0
  46. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/__init__.py +0 -0
  47. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/account_routing.py +0 -0
  48. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/algo.py +0 -0
  49. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/algo_broker.py +0 -0
  50. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/batch_orders.py +0 -0
  51. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/build_info.py +0 -0
  52. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/callback_dispatcher.py +0 -0
  53. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/cftrader.py +0 -0
  54. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/channels.py +0 -0
  55. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/cli.py +0 -0
  56. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/client.py +0 -0
  57. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/config.py +0 -0
  58. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/diagnostics.py +0 -0
  59. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/flow_control.py +0 -0
  60. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/hub.py +0 -0
  61. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/level2.py +0 -0
  62. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/log_management.py +0 -0
  63. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/logging_i18n.py +0 -0
  64. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/management.py +0 -0
  65. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/order_identity.py +0 -0
  66. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/order_meta.py +0 -0
  67. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/pipe_bridge.py +0 -0
  68. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/pipe_client.py +0 -0
  69. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/pipe_hub.py +0 -0
  70. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/pipe_transport.py +0 -0
  71. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/qmt_binding_settings.py +0 -0
  72. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/qmt_bridge.py +0 -0
  73. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/qmt_component_agent.js +0 -0
  74. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/qmt_credentials.py +0 -0
  75. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/qmt_pipe_runtime.py +0 -0
  76. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/qmt_strategy_agent.js +0 -0
  77. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/qmt_strategy_deploy.py +0 -0
  78. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/qmt_strategy_package.py +0 -0
  79. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/qmt_strategy_runtime.py +0 -0
  80. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/qmt_update.py +0 -0
  81. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/quote_history.py +0 -0
  82. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/quote_leases.py +0 -0
  83. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/quote_service.py +0 -0
  84. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/remote_adapter.py +0 -0
  85. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/remote_management.py +0 -0
  86. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/remote_supervisor.py +0 -0
  87. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/runtime_report.py +0 -0
  88. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/shared_quotes.py +0 -0
  89. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/stock_connect.py +0 -0
  90. {cfquant-0.2.59 → cfquant-0.2.60}/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
  91. {cfquant-0.2.59 → cfquant-0.2.60}/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
  92. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/12_/350/256/242/345/215/225/345/244/207/346/263/250/346/265/213/350/257/225.py" +0 -0
  93. {cfquant-0.2.59 → cfquant-0.2.60}/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
  94. {cfquant-0.2.59 → cfquant-0.2.60}/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
  95. {cfquant-0.2.59 → cfquant-0.2.60}/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
  96. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/16_/345/220/257/345/212/250/345/256/211/350/243/205/346/265/213/350/257/225.py" +0 -0
  97. {cfquant-0.2.59 → cfquant-0.2.60}/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
  98. {cfquant-0.2.59 → cfquant-0.2.60}/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
  99. {cfquant-0.2.59 → cfquant-0.2.60}/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
  100. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/1_/350/241/214/346/203/205/346/216/245/346/224/266/346/265/213/350/257/225.py" +0 -0
  101. {cfquant-0.2.59 → cfquant-0.2.60}/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
  102. {cfquant-0.2.59 → cfquant-0.2.60}/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
  103. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/22_/350/277/236/346/216/245/350/257/212/346/226/255/346/265/213/350/257/225.py" +0 -0
  104. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/23_/347/244/272/344/276/213/351/205/215/347/275/256/346/265/213/350/257/225.py" +0 -0
  105. {cfquant-0.2.59 → cfquant-0.2.60}/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
  106. {cfquant-0.2.59 → cfquant-0.2.60}/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
  107. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/26_/345/205/250/346/216/250/350/256/242/351/230/205/346/265/213/350/257/225.py" +0 -0
  108. {cfquant-0.2.59 → cfquant-0.2.60}/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
  109. {cfquant-0.2.59 → cfquant-0.2.60}/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
  110. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/29_Level2/350/241/214/346/203/205/346/265/213/350/257/225.py" +0 -0
  111. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/2_/346/225/260/346/215/256/350/216/267/345/217/226/346/265/213/350/257/225.py" +0 -0
  112. {cfquant-0.2.59 → cfquant-0.2.60}/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
  113. {cfquant-0.2.59 → cfquant-0.2.60}/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
  114. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/32_/345/274/202/346/255/245/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  115. {cfquant-0.2.59 → cfquant-0.2.60}/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
  116. {cfquant-0.2.59 → cfquant-0.2.60}/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
  117. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/3_/346/225/260/346/215/256/344/270/213/350/275/275/346/265/213/350/257/225.py" +0 -0
  118. {cfquant-0.2.59 → cfquant-0.2.60}/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
  119. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/5_/347/234/237/345/256/236/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  120. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/6_/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
  121. {cfquant-0.2.59 → cfquant-0.2.60}/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
  122. {cfquant-0.2.59 → cfquant-0.2.60}/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
  123. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/9_/345/217/257/346/222/244/350/256/242/345/215/225/346/265/213/350/257/225.py" +0 -0
  124. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/_helpers.py +0 -0
  125. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/readme.md +0 -0
  126. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_account_query_timing.py +0 -0
  127. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_advanced_parity.py +0 -0
  128. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_algo.py +0 -0
  129. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_algo_broker.py +0 -0
  130. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_api_tester_browser.py +0 -0
  131. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_api_tester_web.py +0 -0
  132. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
  133. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_binding_live_refresh_browser.py +0 -0
  134. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_binding_runtime_browser.py +0 -0
  135. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_callback_recovery.py +0 -0
  136. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_callback_requests.py +0 -0
  137. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_cftrader.py +0 -0
  138. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_cftrader_tutorial.py +0 -0
  139. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_cftrader_web.py +0 -0
  140. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_credit_compat.py +0 -0
  141. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_dataframe_encoding.py +0 -0
  142. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_json_serialization.py +0 -0
  143. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_latency_flow_control.py +0 -0
  144. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_lite_deployment_paths.py +0 -0
  145. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_lite_parity.py +0 -0
  146. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_lite_qmt_dispatch.py +0 -0
  147. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_local_trading_calendar.py +0 -0
  148. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_log_management.py +0 -0
  149. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_management.py +0 -0
  150. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_management_batch.py +0 -0
  151. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_market_data_fallback.py +0 -0
  152. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_market_timing.py +0 -0
  153. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_order_identity.py +0 -0
  154. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_order_time_callback.py +0 -0
  155. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_project_updater.py +0 -0
  156. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_python_api_reference.py +0 -0
  157. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_qmt_binding_settings.py +0 -0
  158. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_qmt_fileless.py +0 -0
  159. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_qmt_strategy_deploy.py +0 -0
  160. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_qmt_update.py +0 -0
  161. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_quote_latency.py +0 -0
  162. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_quote_latency_encoding.py +0 -0
  163. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_quote_service.py +0 -0
  164. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_remote_management.py +0 -0
  165. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_remote_management_browser.py +0 -0
  166. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_remote_version_check.py +0 -0
  167. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
  168. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_shared_quotes.py +0 -0
  169. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_shared_quotes_generated.py +0 -0
  170. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_startup_recovery.py +0 -0
  171. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_stock_connect.py +0 -0
  172. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_stock_connect_browser.py +0 -0
  173. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_table_conversion.py +0 -0
  174. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_trade_strategy_callback.py +0 -0
  175. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_trading_dates.py +0 -0
  176. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_trading_monitor.py +0 -0
  177. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_trading_monitor_browser.py +0 -0
  178. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_tutorial_reader.py +0 -0
  179. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_tx_auto_import.py +0 -0
  180. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tests/test_tx_shutdown.py +0 -0
  181. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/trading_calendar.py +0 -0
  182. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/trading_monitor.py +0 -0
  183. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/tx.py +0 -0
  184. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/xtconstant.py +0 -0
  185. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/xtdata.py +0 -0
  186. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant/xttype.py +0 -0
  187. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant.egg-info/dependency_links.txt +0 -0
  188. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant.egg-info/entry_points.txt +0 -0
  189. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant.egg-info/top_level.txt +0 -0
  190. {cfquant-0.2.59 → cfquant-0.2.60}/cfquant_pipe_hub.py +0 -0
  191. {cfquant-0.2.59 → cfquant-0.2.60}/qmt_scripts/CFQUANT.py +0 -0
  192. {cfquant-0.2.59 → cfquant-0.2.60}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +0 -0
  193. {cfquant-0.2.59 → cfquant-0.2.60}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
  194. {cfquant-0.2.59 → cfquant-0.2.60}/qmt_scripts/__init__.py +0 -0
  195. {cfquant-0.2.59 → cfquant-0.2.60}/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
  196. {cfquant-0.2.59 → cfquant-0.2.60}/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
  197. {cfquant-0.2.59 → cfquant-0.2.60}/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
  198. {cfquant-0.2.59 → cfquant-0.2.60}/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
  199. {cfquant-0.2.59 → cfquant-0.2.60}/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
  200. {cfquant-0.2.59 → cfquant-0.2.60}/setup.cfg +0 -0
  201. {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/__init__.py +0 -0
  202. {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/api-tester.js +0 -0
  203. {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/avatars/amber-pulse.svg +0 -0
  204. {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/avatars/copper-grid.svg +0 -0
  205. {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/avatars/market-blue.svg +0 -0
  206. {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/avatars/rose-circuit.svg +0 -0
  207. {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/avatars/signal-green.svg +0 -0
  208. {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/avatars/slate-wave.svg +0 -0
  209. {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/avatars/teal-orbit.svg +0 -0
  210. {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/avatars/violet-node.svg +0 -0
  211. {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/cftrader-api-data.js +0 -0
  212. {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/docs.js +0 -0
  213. {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/favicon.svg +0 -0
  214. {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
  215. {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
  216. {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
  217. {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
  218. {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
  219. {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
  220. {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
  221. {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
  222. {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
  223. {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
  224. {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
  225. {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
  226. {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
  227. {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
  228. {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
  229. {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
  230. {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
  231. {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
  232. {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/python-api-data.js +0 -0
  233. {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/python-api.js +0 -0
  234. {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/remote-management.js +0 -0
  235. {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/styles.css +0 -0
  236. {cfquant-0.2.59 → cfquant-0.2.60}/web_dashboard/trading-monitor.js +0 -0
  237. {cfquant-0.2.59 → cfquant-0.2.60}/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.59
3
+ Version: 0.2.60
4
4
  Summary: QMT local bridge and xtquant-compatible Python client.
5
5
  License: MIT
6
6
  Requires-Python: >=3.8
@@ -15,7 +15,7 @@ Requires-Dist: pytz
15
15
  Requires-Dist: tabulate
16
16
  Requires-Dist: pandas-market-calendars
17
17
  Requires-Dist: importlib-resources>=5
18
- Requires-Dist: frida<18,>=17.0; sys_platform == "win32"
18
+ Requires-Dist: frida==17.16.4; sys_platform == "win32"
19
19
  Requires-Dist: pywin32>=306; sys_platform == "win32"
20
20
  Provides-Extra: dev
21
21
  Requires-Dist: build>=1.0; extra == "dev"
@@ -10,9 +10,9 @@ import tempfile
10
10
  from pathlib import Path
11
11
 
12
12
  if __package__:
13
- from .qmt_dependencies import report_startup_dependencies
13
+ from .qmt_dependencies import FRIDA_REQUIREMENT, FRIDA_VERSION, report_startup_dependencies
14
14
  else:
15
- from qmt_dependencies import report_startup_dependencies
15
+ from qmt_dependencies import FRIDA_REQUIREMENT, FRIDA_VERSION, report_startup_dependencies
16
16
 
17
17
 
18
18
  PACKAGE_NAME = "cfquant"
@@ -517,6 +517,32 @@ def installed_package_version(python_exe=None):
517
517
  return completed.stdout.strip() if completed.returncode == 0 else ""
518
518
 
519
519
 
520
+ def reconcile_startup_frida(project_root, python_exe):
521
+ """Repair version drift even when cfquant's own version did not change.
522
+
523
+ This launcher runs before Web/Frida loads. Failed optional repair must not
524
+ prevent manual QMT use; the native worker also checks before importing.
525
+ """
526
+ report = report_startup_dependencies(python_exe) or {}
527
+ if os.environ.get("CFQUANT_QMT_AUTOMATION", "1").lower() in ("0", "false", "off"):
528
+ return
529
+ if not report.get("supported") or not report.get("packages"):
530
+ return
531
+ if report["packages"].get("frida") == FRIDA_VERSION:
532
+ return
533
+ print("[INFO] Installing QMT compatibility dependency: %s" % FRIDA_REQUIREMENT)
534
+ command = [python_exe, "-m", "pip", "install", "--disable-pip-version-check", "--no-input",
535
+ "--timeout", "15", "--retries", "1"] + pip_index_args() + [FRIDA_REQUIREMENT]
536
+ try:
537
+ completed = subprocess.run(command, cwd=str(project_root), env=pip_environment(),
538
+ timeout=120, **_hidden_subprocess_kwargs())
539
+ if completed.returncode:
540
+ raise RuntimeError("pip exit code %s" % completed.returncode)
541
+ except (OSError, RuntimeError, subprocess.TimeoutExpired) as error:
542
+ print("[WARN] Frida compatibility repair failed: %s. Web can continue; use manual QMT login." % error)
543
+ report_startup_dependencies(python_exe)
544
+
545
+
520
546
  def ensure_cfquant_installed(project_root, python_exe=None):
521
547
  project_root = Path(project_root).resolve()
522
548
  if not (project_root / "pyproject.toml").is_file():
@@ -546,7 +572,7 @@ def ensure_cfquant_installed(project_root, python_exe=None):
546
572
  print("cfquant installed=%s project=%s" % (installed or "missing", expected))
547
573
  if installed == expected:
548
574
  print("Package versions match; skip installation.")
549
- report_startup_dependencies(python_exe)
575
+ reconcile_startup_frida(project_root, python_exe)
550
576
  return 0
551
577
 
552
578
  command = editable_install_args(project_root, python_exe)
@@ -570,7 +596,7 @@ def ensure_cfquant_installed(project_root, python_exe=None):
570
596
  print("[ERROR] Installed cfquant version does not match the project after installation.", file=sys.stderr)
571
597
  return 1
572
598
  print("%s package is installed." % PACKAGE_NAME)
573
- report_startup_dependencies(python_exe)
599
+ reconcile_startup_frida(project_root, python_exe)
574
600
  return 0
575
601
 
576
602
 
@@ -194,7 +194,7 @@ class NormalQmtBridge(TxTradeBridge):
194
194
  if action == "cfquant.status":
195
195
  self._send_response(msg, self._status())
196
196
  return
197
- if action == "cfquant.strategy.control":
197
+ if action in ("cfquant.strategy.control", "cfquant.account_subscription_status"):
198
198
  try:
199
199
  self._send_response(msg, self._dispatch(action, msg.get("params") or {}, msg))
200
200
  except Exception as error:
@@ -154,7 +154,8 @@ def is_readonly_request(action):
154
154
  # Only known query families. Downloads, subscriptions and all writes retain
155
155
  # their existing execution semantics, including on an ambiguous timeout.
156
156
  return (str(action).startswith(("xtdata.get_", "xttrader.query_"))
157
- or action in ("cfquant.ping", "cfquant.status", "cfquant.query_info"))
157
+ or action in ("cfquant.ping", "cfquant.status", "cfquant.query_info",
158
+ "cfquant.account_subscription_status"))
158
159
 
159
160
 
160
161
  def request_expired(msg, now=None):
@@ -0,0 +1,54 @@
1
+ """One-time migration of QMT automation preferences, using saved version evidence."""
2
+ import copy
3
+ import re
4
+
5
+
6
+ AUTOMATION_DEFAULTS_VERSION = "0.2.59"
7
+ MIGRATION_KEY = "qmt_automation_defaults_migration"
8
+
9
+
10
+ def is_pre_automation_defaults_version(value):
11
+ """Schema numbers, missing values and unrecognized versions are not evidence."""
12
+ match = re.fullmatch(r"v?(\d+)\.(\d+)\.(\d+)(?:(a|b|rc)\d+)?(?:\.post\d+)?(?:\+[-\w.]+)?", str(value or "").strip())
13
+ if not match:
14
+ return False
15
+ version = tuple(int(match.group(i)) for i in (1, 2, 3))
16
+ return version < (0, 2, 59) or (version == (0, 2, 59) and bool(match.group(4)))
17
+
18
+
19
+ def migrate_qmt_automation_defaults(raw):
20
+ """Return a copy only when a known old configuration needs migration.
21
+
22
+ The running/installed version and QMT's version do not date user preferences.
23
+ They must never be used as substitutes for the saved configuration version.
24
+ """
25
+ if not isinstance(raw, dict) or raw.get(MIGRATION_KEY):
26
+ return raw, 0
27
+ source_version = raw.get("saved_core_version", raw.get("core_version"))
28
+ if not is_pre_automation_defaults_version(source_version):
29
+ return raw, 0
30
+ result = copy.deepcopy(raw)
31
+ changed = 0
32
+ for collection in ("account_configs", "account_pairs"):
33
+ rows = result.get(collection) or {}
34
+ rows = rows.values() if isinstance(rows, dict) else rows if isinstance(rows, list) else []
35
+ for row in rows:
36
+ if not isinstance(row, dict) or not row.get("account_id"):
37
+ continue
38
+ login = row.get("qmt_auto_login")
39
+ login = dict(login) if isinstance(login, dict) else {}
40
+ login.update(enabled=True, auto_update=True)
41
+ # Preserve custom schedules and explicit empty schedules. Only absent
42
+ # schedules receive the same default as a new binding.
43
+ if not any(key in login for key in ("restart_times", "restartTimes", "restart_time", "restartTime")):
44
+ login["restart_times"] = ["09:00"]
45
+ row["qmt_auto_login"] = login
46
+ if collection == "account_configs":
47
+ strategy = row.get("qmt_strategy")
48
+ strategy = dict(strategy) if isinstance(strategy, dict) else {}
49
+ strategy.update(enabled=True, autorun=True)
50
+ row["qmt_strategy"] = strategy
51
+ changed += 1
52
+ result[MIGRATION_KEY] = {"version": AUTOMATION_DEFAULTS_VERSION, "source_version": str(source_version),
53
+ "bindings": changed}
54
+ return result, changed
@@ -11,6 +11,8 @@ from pathlib import Path
11
11
  import sys
12
12
  import time
13
13
 
14
+ from .qmt_native_worker import IsolatedLoginBackend, QmtAutomationCancelled, verify_environment_isolated
15
+
14
16
 
15
17
  class QmtNotReady(RuntimeError):
16
18
  pass
@@ -22,6 +24,11 @@ class QmtProcessExited(RuntimeError):
22
24
 
23
25
  def verify_environment(directory):
24
26
  """Validate dependencies and native ABI before stopping a running terminal."""
27
+ verify_environment_isolated(directory)
28
+
29
+
30
+ def _verify_environment_native(directory):
31
+ """Only invoked inside the native worker or the standalone login CLI."""
25
32
  try:
26
33
  from .qmt_dependencies import LOGIN_MODULES, import_login_module
27
34
  except ImportError:
@@ -163,7 +170,7 @@ class QtLoginBackend:
163
170
  self.pid = matches[0].pid
164
171
  if expected_pid is not None and self.pid != expected_pid:
165
172
  raise RuntimeError("QMT process changed; stopping this login attempt")
166
- verify_environment(directory)
173
+ _verify_environment_native(directory)
167
174
  self.mutex = win32event.CreateMutex(None, False, "Local\\CFQuantQmtComponentLogin_" + str(self.pid))
168
175
  acquired = win32event.WaitForSingleObject(self.mutex, 0)
169
176
  if acquired not in (0, 0x80):
@@ -367,8 +374,11 @@ def login_running_qmt(directory, login_account="", password_provider=lambda: "",
367
374
  if should_cancel():
368
375
  return {"outcome": "cancelled", "login_succeeded": False, "attempts": []}
369
376
  try:
370
- backend = QtLoginBackend(Path(directory), timeout, expected_pid=expected_pid)
377
+ backend = IsolatedLoginBackend(Path(directory), timeout, expected_pid=expected_pid,
378
+ should_cancel=should_cancel)
371
379
  break
380
+ except QmtAutomationCancelled:
381
+ return {"outcome": "cancelled", "login_succeeded": False, "attempts": []}
372
382
  except QmtProcessExited:
373
383
  return {"outcome": "process_exited", "login_succeeded": False, "attempts": []}
374
384
  except QmtNotReady:
@@ -380,6 +390,8 @@ def login_running_qmt(directory, login_account="", password_provider=lambda: "",
380
390
  backend.should_cancel = should_cancel
381
391
  return auto_login(backend, password_provider, retry_not_ready=True, login_account=login_account,
382
392
  should_cancel=should_cancel, on_wait=on_wait)
393
+ except QmtAutomationCancelled:
394
+ return {"outcome": "cancelled", "login_succeeded": False, "attempts": []}
383
395
  except QmtProcessExited:
384
396
  return {"outcome": "process_exited", "login_succeeded": False, "attempts": []}
385
397
  finally:
@@ -1,6 +1,7 @@
1
1
  """Read-only checks and repair instructions for optional QMT login components."""
2
2
  import copy
3
3
  import importlib
4
+ import importlib.metadata
4
5
  import json
5
6
  import os
6
7
  import subprocess
@@ -10,6 +11,10 @@ import time
10
11
 
11
12
 
12
13
  LOGIN_MODULES = ("frida", "win32api", "win32crypt", "win32event", "win32gui", "win32process")
14
+ # Verified QMT baseline. 17.25 introduced a Windows mapper that can abort the
15
+ # host on older ntdll builds; do not widen this without Windows/QMT validation.
16
+ FRIDA_VERSION = "17.16.4"
17
+ FRIDA_REQUIREMENT = "frida==" + FRIDA_VERSION
13
18
  _LOCK = threading.Lock()
14
19
  _CACHE = {}
15
20
  _PROBE_CODE = r'''
@@ -17,6 +22,8 @@ import importlib, importlib.metadata, json, os, pathlib, sys
17
22
  checks = []
18
23
  for name in MODULES:
19
24
  try:
25
+ if name == 'frida' and importlib.metadata.version('frida') != FRIDA_VERSION:
26
+ raise RuntimeError('Unsupported Frida version; install frida==' + FRIDA_VERSION)
20
27
  module = importlib.import_module(name)
21
28
  checks.append(dict(name=name, ok=True, path=getattr(module, '__file__', '')))
22
29
  except Exception as error:
@@ -46,6 +53,16 @@ def _ps_quote(value):
46
53
 
47
54
 
48
55
  def _finish_report(report):
56
+ if report.get("supported"):
57
+ version = (report.get("packages") or {}).get("frida")
58
+ if version and version != FRIDA_VERSION:
59
+ checks = report.setdefault("checks", [])
60
+ check = next((item for item in checks if item["name"] == "frida"), None)
61
+ if check is None:
62
+ check = {"name": "frida"}
63
+ checks.append(check)
64
+ check.update(ok=False, error="Frida %s 未通过本项目兼容性验证,请安装 %s" % (version, FRIDA_REQUIREMENT))
65
+ report["frida_requirement"] = FRIDA_REQUIREMENT
49
66
  report["checked_at"] = time.time()
50
67
  report["ok"] = bool(report.get("supported") and not report.get("error")
51
68
  and all(item["ok"] for item in report.get("checks", [])))
@@ -73,8 +90,10 @@ def _finish_report(report):
73
90
  if environment == "python":
74
91
  commands.append(python + " -m pywin32_postinstall -install")
75
92
  if any(item["name"] == "frida" for item in failed):
76
- commands.append(python + " -m pip install --no-cache-dir --upgrade --force-reinstall 'frida>=17.0,<18'")
77
- commands.append(python + " -c " + _ps_quote("import " + ", ".join(LOGIN_MODULES) + "; print('QMT login components OK')"))
93
+ commands.append(python + " -m pip install --no-cache-dir --force-reinstall '" + FRIDA_REQUIREMENT + "'")
94
+ commands.append(python + " -c " + _ps_quote(
95
+ "from cfquant.qmt_dependencies import report_startup_dependencies; "
96
+ "r=report_startup_dependencies(); raise SystemExit(0 if r['ok'] else 1)"))
78
97
  report["repair_commands"] = commands
79
98
  if environment == "conda":
80
99
  report["repair_note"] = (
@@ -111,7 +130,7 @@ def check_login_dependencies(python_exe=None, refresh=False, timeout=15):
111
130
  if os.name == "nt":
112
131
  run_kwargs["creationflags"] = getattr(subprocess, "CREATE_NO_WINDOW", 0)
113
132
  completed = subprocess.run(
114
- [executable, "-c", "MODULES = %r\n" % (LOGIN_MODULES,) + _PROBE_CODE],
133
+ [executable, "-c", "MODULES = %r\nFRIDA_VERSION = %r\n" % (LOGIN_MODULES, FRIDA_VERSION) + _PROBE_CODE],
115
134
  stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding="utf-8",
116
135
  errors="replace", timeout=timeout,
117
136
  **run_kwargs,
@@ -130,8 +149,21 @@ class QmtDependencyError(RuntimeError):
130
149
  pass
131
150
 
132
151
 
152
+ def require_compatible_frida():
153
+ try:
154
+ version = importlib.metadata.version("frida")
155
+ except importlib.metadata.PackageNotFoundError:
156
+ version = "未安装"
157
+ if version != FRIDA_VERSION:
158
+ raise QmtDependencyError(
159
+ "Frida %s 未通过本项目兼容性验证,已跳过 QMT 自动操作。"
160
+ "请在当前 Python 环境安装 %s;手动登录及行情交易接口仍可使用。" % (version, FRIDA_REQUIREMENT))
161
+
162
+
133
163
  def import_login_module(name):
134
164
  """Keep in-process import errors actionable, including stale loaded DLLs."""
165
+ if name == "frida":
166
+ require_compatible_frida()
135
167
  try:
136
168
  return importlib.import_module(name)
137
169
  except (ImportError, OSError) as error:
@@ -0,0 +1,253 @@
1
+ """Isolate QMT/Frida native code from the Web process using private stdio RPC.
2
+
3
+ Credentials only travel over the child's stdin, never argv, files or logs.
4
+ Requests are not replayed after a crash or timeout: a native action may have run.
5
+ """
6
+ import json
7
+ import os
8
+ from pathlib import Path
9
+ import queue
10
+ import subprocess
11
+ import sys
12
+ import threading
13
+ import time
14
+
15
+
16
+ LOGIN_METHODS = frozenset(("already_logged_in", "state", "select_account", "submit",
17
+ "fill", "wait_outcome", "acknowledge_failure"))
18
+ STRATEGY_METHODS = frozenset(("snapshot", "start_strategy", "notices", "dismiss_notice"))
19
+ MAX_MESSAGE = 4 * 1024 * 1024
20
+
21
+
22
+ class QmtAutomationError(RuntimeError):
23
+ """A failed native worker; safe to display without native exception contents."""
24
+
25
+
26
+ class QmtAutomationCancelled(QmtAutomationError):
27
+ pass
28
+
29
+
30
+ class NativeWorker:
31
+ def __init__(self, kind, directory, *, timeout=40, expected_pid=None,
32
+ should_cancel=lambda: False, command=None):
33
+ if os.environ.get("CFQUANT_QMT_AUTOMATION", "1").lower() in ("0", "false", "off"):
34
+ raise QmtAutomationError("QMT 原生自动化已关闭,请手动登录并启动策略;Web 服务及行情交易接口仍可使用。")
35
+ self.should_cancel = should_cancel
36
+ self.call_timeout = max(15.0, float(timeout) + 10)
37
+ self.methods = LOGIN_METHODS if kind == "login" else STRATEGY_METHODS
38
+ self._lock = threading.RLock()
39
+ self._responses = queue.Queue()
40
+ self._sequence = 0
41
+ self._closed = False
42
+ self._tls_error = False
43
+ 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
+ )
51
+ self._readers = [
52
+ threading.Thread(target=self._read_responses, name="qmt-native-response", daemon=True),
53
+ threading.Thread(target=self._read_errors, name="qmt-native-stderr", daemon=True),
54
+ ]
55
+ for thread in self._readers:
56
+ thread.start()
57
+ try:
58
+ info = self.call("open", kind=kind, directory=str(directory), timeout=timeout,
59
+ expected_pid=expected_pid)
60
+ self.pid = info.get("pid")
61
+ self.main_hwnd = info.get("main_hwnd")
62
+ except BaseException:
63
+ self.close(force=True)
64
+ raise
65
+
66
+ def _read_responses(self):
67
+ try:
68
+ while True:
69
+ line = self.process.stdout.readline(MAX_MESSAGE + 1)
70
+ if not line or len(line) > MAX_MESSAGE:
71
+ break
72
+ self._responses.put(json.loads(line))
73
+ except (ValueError, OSError):
74
+ pass
75
+ finally:
76
+ self._responses.put(None)
77
+
78
+ def _read_errors(self):
79
+ try:
80
+ while True:
81
+ chunk = self.process.stderr.readline(4096)
82
+ if not chunk:
83
+ break
84
+ if "Unable to locate the ntdll loader TLS routines" in chunk:
85
+ self._tls_error = True
86
+ # Native output can contain input values; never forward it.
87
+ except (ValueError, OSError):
88
+ pass
89
+
90
+ def _failure(self, reason):
91
+ from .qmt_dependencies import FRIDA_REQUIREMENT
92
+ detail = "Frida 与当前 Windows 不兼容" if self._tls_error else reason
93
+ return QmtAutomationError(
94
+ "QMT 自动化子进程%s,已停止本次自动操作,Web 服务继续运行。"
95
+ "请检查自动登录组件并安装 %s,或手动登录、启动 QMT 策略。" % (detail, FRIDA_REQUIREMENT))
96
+
97
+ def call(self, method, *args, **kwargs):
98
+ with self._lock:
99
+ if self._closed:
100
+ raise self._failure("已关闭")
101
+ self._sequence += 1
102
+ request_id = self._sequence
103
+ try:
104
+ if self.should_cancel():
105
+ raise QmtAutomationCancelled("QMT 自动操作已取消")
106
+ message = json.dumps(dict(id=request_id, method=method, args=args, kwargs=kwargs), ensure_ascii=True)
107
+ if len(message) > MAX_MESSAGE:
108
+ raise self._failure("请求过大")
109
+ self.process.stdin.write(message + "\n")
110
+ self.process.stdin.flush()
111
+ deadline = time.monotonic() + self.call_timeout
112
+ while True:
113
+ if self.should_cancel():
114
+ raise QmtAutomationCancelled("QMT 自动操作已取消")
115
+ remaining = deadline - time.monotonic()
116
+ if remaining <= 0:
117
+ raise self._failure("响应超时")
118
+ try:
119
+ reply = self._responses.get(timeout=min(.1, remaining))
120
+ break
121
+ except queue.Empty:
122
+ continue
123
+ if not isinstance(reply, dict) or reply.get("id") != request_id:
124
+ raise self._failure("异常退出或通信中断")
125
+ except (BrokenPipeError, OSError) as error:
126
+ self.close(force=True)
127
+ raise self._failure("通信中断") from error
128
+ except BaseException:
129
+ self.close(force=True)
130
+ raise
131
+ if not reply.get("ok"):
132
+ self._raise_remote(reply)
133
+ return reply.get("result")
134
+
135
+ @staticmethod
136
+ def _raise_remote(reply):
137
+ from .qmt_component_login import QmtNotReady, QmtProcessExited
138
+ from .qmt_dependencies import QmtDependencyError
139
+ from .qmt_strategy_recovery import UnsupportedQmt
140
+ errors = {cls.__name__: cls for cls in (QmtNotReady, QmtProcessExited, QmtDependencyError, UnsupportedQmt)}
141
+ raise errors.get(reply.get("error_type"), RuntimeError)(reply.get("error") or "QMT native operation failed")
142
+
143
+ def __getattr__(self, name):
144
+ if name not in self.methods:
145
+ raise AttributeError(name)
146
+ return lambda *args, **kwargs: self.call(name, *args, **kwargs)
147
+
148
+ def close(self, force=False):
149
+ with self._lock:
150
+ if self._closed:
151
+ return
152
+ self._closed = True
153
+ try:
154
+ if not force and self.process.poll() is None:
155
+ self.process.stdin.write(json.dumps(dict(id=0, method="close")) + "\n")
156
+ self.process.stdin.flush()
157
+ self.process.wait(timeout=1)
158
+ except (OSError, ValueError, subprocess.TimeoutExpired):
159
+ pass
160
+ finally:
161
+ if self.process.poll() is None:
162
+ self.process.kill()
163
+ self.process.wait(timeout=5)
164
+ try:
165
+ self.process.stdin.close()
166
+ except OSError:
167
+ pass # A crash may leave buffered input with no reader.
168
+ for thread in self._readers:
169
+ thread.join(timeout=1)
170
+ self.process.stdout.close()
171
+ self.process.stderr.close()
172
+
173
+
174
+ class IsolatedLoginBackend(NativeWorker):
175
+ def __init__(self, directory, timeout=40, expected_pid=None, *, should_cancel=lambda: False):
176
+ super().__init__("login", directory, timeout=timeout, expected_pid=expected_pid, should_cancel=should_cancel)
177
+
178
+
179
+ class IsolatedStrategyBackend(NativeWorker):
180
+ def __init__(self, directory):
181
+ super().__init__("strategy", directory, timeout=15)
182
+
183
+
184
+ def verify_environment_isolated(directory):
185
+ worker = NativeWorker("verify", directory, timeout=5)
186
+ worker.close()
187
+
188
+
189
+ def _open_backend(kind, directory, timeout=40, expected_pid=None):
190
+ from .qmt_component_login import QtLoginBackend, _verify_environment_native
191
+ if kind == "verify":
192
+ _verify_environment_native(directory)
193
+ return None
194
+ if kind == "login":
195
+ return QtLoginBackend(Path(directory), timeout, expected_pid=expected_pid)
196
+ if kind == "strategy":
197
+ from .qmt_strategy_recovery import QtStrategyBackend
198
+ return QtStrategyBackend(directory)
199
+ raise ValueError("Unknown QMT worker kind")
200
+
201
+
202
+ def serve(factory=_open_backend):
203
+ # Reserve stdout for protocol, including when a native DLL prints to fd 1.
204
+ output = os.fdopen(os.dup(sys.stdout.fileno()), "w", encoding="utf-8", buffering=1)
205
+ os.dup2(sys.stderr.fileno(), sys.stdout.fileno())
206
+ sys.stdout = sys.stderr
207
+ requests = queue.Queue(maxsize=2)
208
+ def receive():
209
+ try:
210
+ while True:
211
+ line = sys.stdin.readline(MAX_MESSAGE + 1)
212
+ if not line or len(line) > MAX_MESSAGE:
213
+ break
214
+ requests.put(json.loads(line))
215
+ finally:
216
+ # Also stop a hung native call if the Web parent exits unexpectedly.
217
+ os._exit(0)
218
+ threading.Thread(target=receive, name="qmt-native-input", daemon=True).start()
219
+ backend = None
220
+ opened = False
221
+ methods = frozenset()
222
+ try:
223
+ while True:
224
+ request = requests.get()
225
+ method = request.get("method")
226
+ if method == "close":
227
+ break
228
+ try:
229
+ if method == "open" and not opened:
230
+ kwargs = request["kwargs"]
231
+ backend = factory(**kwargs)
232
+ opened = True
233
+ methods = LOGIN_METHODS if kwargs["kind"] == "login" else STRATEGY_METHODS
234
+ result = {key: getattr(backend, key, None) for key in ("pid", "main_hwnd")}
235
+ elif opened and backend is not None and method in methods:
236
+ result = getattr(backend, method)(*request.get("args", []), **request.get("kwargs", {}))
237
+ else:
238
+ raise ValueError("Unsupported QMT worker request")
239
+ reply = dict(id=request["id"], ok=True, result=result)
240
+ except Exception as error:
241
+ reply = dict(id=request["id"], ok=False, error_type=type(error).__name__,
242
+ error="QMT credential entry failed" if method == "fill" else str(error))
243
+ output.write(json.dumps(reply, ensure_ascii=True) + "\n")
244
+ output.flush()
245
+ request = None
246
+ finally:
247
+ if backend is not None:
248
+ backend.close()
249
+ output.close()
250
+
251
+
252
+ if __name__ == "__main__":
253
+ serve()
@@ -16,6 +16,8 @@ import time
16
16
  import uuid
17
17
 
18
18
  from .qmt_component_login import QtLoginBackend, QmtNotReady
19
+ from .qmt_native_worker import IsolatedStrategyBackend, QmtAutomationError
20
+ from .qmt_dependencies import QmtDependencyError
19
21
  from .qmt_strategy_deploy import (
20
22
  _role_stock, _strategy_slot_name, account_qmt_roots,
21
23
  _write_json, _digest, PipeStrategyController, normalize_strategy_mode, normalize_strategy_settings, qmt_root,
@@ -166,7 +168,7 @@ def match_models(snapshot, expected):
166
168
 
167
169
 
168
170
  class QmtStrategyRecovery:
169
- def __init__(self, accounts_provider, *, backend_factory=QtStrategyBackend,
171
+ def __init__(self, accounts_provider, *, backend_factory=IsolatedStrategyBackend,
170
172
  job_is_current=lambda job: True, clock=time.time, interval=20.0,
171
173
  notice_probe=visible_notice_windows, event_sink=None, bridge_verifier=confirm_recovery_bridge):
172
174
  self.accounts_provider = accounts_provider
@@ -235,10 +237,12 @@ class QmtStrategyRecovery:
235
237
  return job["recovery"]
236
238
  previous = job.get("recovery") or {}
237
239
  if (not inspect_only and now < previous.get("next_check_at", 0)
238
- and not self.notice_probe(previous.get("pid"), previous.get("main_hwnd"))):
240
+ and (previous.get("native_failure")
241
+ or not self.notice_probe(previous.get("pid"), previous.get("main_hwnd")))):
239
242
  return previous
240
243
  status = dict(previous, last_checked_at=now, next_check_at=now + self.interval)
241
244
  status.pop("error", None)
245
+ status.pop("native_failure", None)
242
246
  backend = None
243
247
  try:
244
248
  backend = self.backend_factory(job["root"])
@@ -303,6 +307,7 @@ class QmtStrategyRecovery:
303
307
  except Exception as error:
304
308
  status.update(state="unsupported" if isinstance(error, UnsupportedQmt) else "unavailable",
305
309
  error=str(error), next_check_at=now + max(self.interval, 60))
310
+ status["native_failure"] = isinstance(error, (QmtAutomationError, QmtDependencyError))
306
311
  if status.get("incident") and status.get("last_logged_error") != str(error):
307
312
  try:
308
313
  self._event(job, status, "illegal_ip_processing_failed", error=str(error))
@@ -38,6 +38,8 @@ class RpcTiming(object):
38
38
  if self.sent_at is not None:
39
39
  self.output["rpc"]["response_wait_ms"] = round((self.received_at - self.sent_at) * 1000, 3)
40
40
  meta = message.get("meta") or {}
41
+ if "web_queue_ms" in meta:
42
+ self.output["web"] = {"queue_ms": meta["web_queue_ms"], "route": meta.get("route")}
41
43
  qmt = (meta.get("timing") or {}).get("qmt")
42
44
  self.output["qmt"] = dict(qmt) if isinstance(qmt, dict) else {
43
45
  "available": False, "reason": "bridge_timing_unavailable",