cfquant 0.2.59__tar.gz → 0.2.61__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 (238) hide show
  1. {cfquant-0.2.59 → cfquant-0.2.61}/PKG-INFO +2 -2
  2. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/_editable_install.py +30 -4
  3. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/normal_bridge.py +1 -1
  4. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/pipe_bridge.py +37 -17
  5. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/pipe_transport.py +102 -36
  6. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/protocol.py +2 -1
  7. cfquant-0.2.61/cfquant/qmt_binding_defaults.py +54 -0
  8. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/qmt_component_login.py +14 -2
  9. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/qmt_dependencies.py +35 -3
  10. cfquant-0.2.61/cfquant/qmt_native_worker.py +253 -0
  11. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/qmt_pipe_runtime.py +42 -16
  12. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/qmt_strategy_recovery.py +7 -2
  13. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/rpc_timing.py +2 -0
  14. cfquant-0.2.61/cfquant/tests/test_account_subscription_probe.py +233 -0
  15. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_lite_account_routing.py +4 -2
  16. cfquant-0.2.61/cfquant/tests/test_pipe_shutdown.py +394 -0
  17. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_qmt_auto_login_browser.py +31 -0
  18. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_qmt_auto_login_integration.py +30 -6
  19. cfquant-0.2.61/cfquant/tests/test_qmt_binding_defaults.py +116 -0
  20. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_qmt_component_login.py +6 -6
  21. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_qmt_dependencies.py +68 -0
  22. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_qmt_fileless.py +15 -1
  23. cfquant-0.2.61/cfquant/tests/test_qmt_native_worker.py +166 -0
  24. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_qmt_strategy_browser.py +1 -1
  25. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_qmt_strategy_recovery.py +20 -0
  26. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_qmt_strategy_web.py +3 -3
  27. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_subscription_congestion.py +7 -6
  28. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_trader_subscription_recovery.py +21 -6
  29. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tx_trade_bridge.py +33 -2
  30. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/version.py +2 -2
  31. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/xttrader.py +43 -7
  32. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant.egg-info/PKG-INFO +2 -2
  33. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant.egg-info/SOURCES.txt +6 -0
  34. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant.egg-info/requires.txt +1 -1
  35. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant_web_server.py +92 -14
  36. {cfquant-0.2.59 → cfquant-0.2.61}/pyproject.toml +2 -2
  37. {cfquant-0.2.59 → cfquant-0.2.61}/qmt_scripts/CFQUANT_LITE.py +262 -118
  38. {cfquant-0.2.59 → cfquant-0.2.61}/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 +262 -118
  39. {cfquant-0.2.59 → cfquant-0.2.61}/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 +262 -118
  40. {cfquant-0.2.59 → cfquant-0.2.61}/requirements.txt +1 -1
  41. {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/app.js +5 -5
  42. {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/index.html +8 -8
  43. {cfquant-0.2.59 → cfquant-0.2.61}/LICENSE +0 -0
  44. {cfquant-0.2.59 → cfquant-0.2.61}/LTtx/__init__.py +0 -0
  45. {cfquant-0.2.59 → cfquant-0.2.61}/LTtx/tx/Config.txt +0 -0
  46. {cfquant-0.2.59 → cfquant-0.2.61}/LTtx/tx/LTtx_server.py +0 -0
  47. {cfquant-0.2.59 → cfquant-0.2.61}/LTtx/tx/__init__.py +0 -0
  48. {cfquant-0.2.59 → cfquant-0.2.61}/LTtx/tx/tx.py +0 -0
  49. {cfquant-0.2.59 → cfquant-0.2.61}/MANIFEST.in +0 -0
  50. {cfquant-0.2.59 → cfquant-0.2.61}/README.md +0 -0
  51. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/__init__.py +0 -0
  52. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/account_routing.py +0 -0
  53. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/algo.py +0 -0
  54. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/algo_broker.py +0 -0
  55. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/batch_orders.py +0 -0
  56. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/build_info.py +0 -0
  57. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/callback_dispatcher.py +0 -0
  58. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/cftrader.py +0 -0
  59. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/channels.py +0 -0
  60. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/cli.py +0 -0
  61. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/client.py +0 -0
  62. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/config.py +0 -0
  63. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/diagnostics.py +0 -0
  64. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/flow_control.py +0 -0
  65. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/hub.py +0 -0
  66. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/level2.py +0 -0
  67. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/log_management.py +0 -0
  68. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/logging_i18n.py +0 -0
  69. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/management.py +0 -0
  70. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/order_identity.py +0 -0
  71. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/order_meta.py +0 -0
  72. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/pipe_client.py +0 -0
  73. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/pipe_hub.py +0 -0
  74. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/qmt_binding_settings.py +0 -0
  75. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/qmt_bridge.py +0 -0
  76. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/qmt_component_agent.js +0 -0
  77. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/qmt_credentials.py +0 -0
  78. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/qmt_strategy_agent.js +0 -0
  79. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/qmt_strategy_deploy.py +0 -0
  80. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/qmt_strategy_package.py +0 -0
  81. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/qmt_strategy_runtime.py +0 -0
  82. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/qmt_update.py +0 -0
  83. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/quote_history.py +0 -0
  84. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/quote_leases.py +0 -0
  85. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/quote_service.py +0 -0
  86. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/remote_adapter.py +0 -0
  87. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/remote_management.py +0 -0
  88. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/remote_supervisor.py +0 -0
  89. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/runtime_report.py +0 -0
  90. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/shared_quotes.py +0 -0
  91. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/stock_connect.py +0 -0
  92. {cfquant-0.2.59 → cfquant-0.2.61}/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
  93. {cfquant-0.2.59 → cfquant-0.2.61}/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
  94. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/12_/350/256/242/345/215/225/345/244/207/346/263/250/346/265/213/350/257/225.py" +0 -0
  95. {cfquant-0.2.59 → cfquant-0.2.61}/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
  96. {cfquant-0.2.59 → cfquant-0.2.61}/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
  97. {cfquant-0.2.59 → cfquant-0.2.61}/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
  98. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/16_/345/220/257/345/212/250/345/256/211/350/243/205/346/265/213/350/257/225.py" +0 -0
  99. {cfquant-0.2.59 → cfquant-0.2.61}/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
  100. {cfquant-0.2.59 → cfquant-0.2.61}/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
  101. {cfquant-0.2.59 → cfquant-0.2.61}/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
  102. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/1_/350/241/214/346/203/205/346/216/245/346/224/266/346/265/213/350/257/225.py" +0 -0
  103. {cfquant-0.2.59 → cfquant-0.2.61}/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
  104. {cfquant-0.2.59 → cfquant-0.2.61}/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
  105. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/22_/350/277/236/346/216/245/350/257/212/346/226/255/346/265/213/350/257/225.py" +0 -0
  106. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/23_/347/244/272/344/276/213/351/205/215/347/275/256/346/265/213/350/257/225.py" +0 -0
  107. {cfquant-0.2.59 → cfquant-0.2.61}/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
  108. {cfquant-0.2.59 → cfquant-0.2.61}/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
  109. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/26_/345/205/250/346/216/250/350/256/242/351/230/205/346/265/213/350/257/225.py" +0 -0
  110. {cfquant-0.2.59 → cfquant-0.2.61}/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
  111. {cfquant-0.2.59 → cfquant-0.2.61}/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
  112. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/29_Level2/350/241/214/346/203/205/346/265/213/350/257/225.py" +0 -0
  113. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/2_/346/225/260/346/215/256/350/216/267/345/217/226/346/265/213/350/257/225.py" +0 -0
  114. {cfquant-0.2.59 → cfquant-0.2.61}/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
  115. {cfquant-0.2.59 → cfquant-0.2.61}/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
  116. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/32_/345/274/202/346/255/245/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  117. {cfquant-0.2.59 → cfquant-0.2.61}/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
  118. {cfquant-0.2.59 → cfquant-0.2.61}/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
  119. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/3_/346/225/260/346/215/256/344/270/213/350/275/275/346/265/213/350/257/225.py" +0 -0
  120. {cfquant-0.2.59 → cfquant-0.2.61}/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
  121. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/5_/347/234/237/345/256/236/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  122. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/6_/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
  123. {cfquant-0.2.59 → cfquant-0.2.61}/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
  124. {cfquant-0.2.59 → cfquant-0.2.61}/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
  125. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/9_/345/217/257/346/222/244/350/256/242/345/215/225/346/265/213/350/257/225.py" +0 -0
  126. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/_helpers.py +0 -0
  127. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/readme.md +0 -0
  128. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_account_query_timing.py +0 -0
  129. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_advanced_parity.py +0 -0
  130. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_algo.py +0 -0
  131. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_algo_broker.py +0 -0
  132. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_api_tester_browser.py +0 -0
  133. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_api_tester_web.py +0 -0
  134. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
  135. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_binding_live_refresh_browser.py +0 -0
  136. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_binding_runtime_browser.py +0 -0
  137. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_callback_recovery.py +0 -0
  138. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_callback_requests.py +0 -0
  139. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_cftrader.py +0 -0
  140. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_cftrader_tutorial.py +0 -0
  141. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_cftrader_web.py +0 -0
  142. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_credit_compat.py +0 -0
  143. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_dataframe_encoding.py +0 -0
  144. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_json_serialization.py +0 -0
  145. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_latency_flow_control.py +0 -0
  146. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_lite_deployment_paths.py +0 -0
  147. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_lite_parity.py +0 -0
  148. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_lite_qmt_dispatch.py +0 -0
  149. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_local_trading_calendar.py +0 -0
  150. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_log_management.py +0 -0
  151. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_management.py +0 -0
  152. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_management_batch.py +0 -0
  153. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_market_data_fallback.py +0 -0
  154. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_market_timing.py +0 -0
  155. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_order_identity.py +0 -0
  156. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_order_time_callback.py +0 -0
  157. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_project_updater.py +0 -0
  158. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_python_api_reference.py +0 -0
  159. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_qmt_binding_settings.py +0 -0
  160. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_qmt_strategy_deploy.py +0 -0
  161. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_qmt_update.py +0 -0
  162. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_quote_latency.py +0 -0
  163. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_quote_latency_encoding.py +0 -0
  164. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_quote_service.py +0 -0
  165. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_remote_management.py +0 -0
  166. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_remote_management_browser.py +0 -0
  167. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_remote_version_check.py +0 -0
  168. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
  169. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_shared_quotes.py +0 -0
  170. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_shared_quotes_generated.py +0 -0
  171. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_startup_recovery.py +0 -0
  172. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_stock_connect.py +0 -0
  173. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_stock_connect_browser.py +0 -0
  174. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_table_conversion.py +0 -0
  175. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_trade_strategy_callback.py +0 -0
  176. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_trading_dates.py +0 -0
  177. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_trading_monitor.py +0 -0
  178. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_trading_monitor_browser.py +0 -0
  179. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_tutorial_reader.py +0 -0
  180. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_tx_auto_import.py +0 -0
  181. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tests/test_tx_shutdown.py +0 -0
  182. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/trading_calendar.py +0 -0
  183. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/trading_monitor.py +0 -0
  184. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/tx.py +0 -0
  185. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/xtconstant.py +0 -0
  186. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/xtdata.py +0 -0
  187. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant/xttype.py +0 -0
  188. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant.egg-info/dependency_links.txt +0 -0
  189. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant.egg-info/entry_points.txt +0 -0
  190. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant.egg-info/top_level.txt +0 -0
  191. {cfquant-0.2.59 → cfquant-0.2.61}/cfquant_pipe_hub.py +0 -0
  192. {cfquant-0.2.59 → cfquant-0.2.61}/qmt_scripts/CFQUANT.py +0 -0
  193. {cfquant-0.2.59 → cfquant-0.2.61}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +0 -0
  194. {cfquant-0.2.59 → cfquant-0.2.61}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
  195. {cfquant-0.2.59 → cfquant-0.2.61}/qmt_scripts/__init__.py +0 -0
  196. {cfquant-0.2.59 → cfquant-0.2.61}/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
  197. {cfquant-0.2.59 → cfquant-0.2.61}/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
  198. {cfquant-0.2.59 → cfquant-0.2.61}/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
  199. {cfquant-0.2.59 → cfquant-0.2.61}/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
  200. {cfquant-0.2.59 → cfquant-0.2.61}/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
  201. {cfquant-0.2.59 → cfquant-0.2.61}/setup.cfg +0 -0
  202. {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/__init__.py +0 -0
  203. {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/api-tester.js +0 -0
  204. {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/avatars/amber-pulse.svg +0 -0
  205. {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/avatars/copper-grid.svg +0 -0
  206. {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/avatars/market-blue.svg +0 -0
  207. {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/avatars/rose-circuit.svg +0 -0
  208. {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/avatars/signal-green.svg +0 -0
  209. {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/avatars/slate-wave.svg +0 -0
  210. {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/avatars/teal-orbit.svg +0 -0
  211. {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/avatars/violet-node.svg +0 -0
  212. {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/cftrader-api-data.js +0 -0
  213. {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/docs.js +0 -0
  214. {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/favicon.svg +0 -0
  215. {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
  216. {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
  217. {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
  218. {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
  219. {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
  220. {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
  221. {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
  222. {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
  223. {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
  224. {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
  225. {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
  226. {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
  227. {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
  228. {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
  229. {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
  230. {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
  231. {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
  232. {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
  233. {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/python-api-data.js +0 -0
  234. {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/python-api.js +0 -0
  235. {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/remote-management.js +0 -0
  236. {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/styles.css +0 -0
  237. {cfquant-0.2.59 → cfquant-0.2.61}/web_dashboard/trading-monitor.js +0 -0
  238. {cfquant-0.2.59 → cfquant-0.2.61}/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.61
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:
@@ -48,8 +48,16 @@ class PipeNormalQmtBridge(NormalQmtBridge):
48
48
  self.pipe_name = pipe_name or DEFAULT_PIPE_NAME
49
49
  self.request_channels = list(request_channels or [request_channel])
50
50
  self.connect_timeout_ms = int(connect_timeout_ms)
51
+ self._pipe_lock = threading.RLock()
52
+ self._pipe_closed = False
51
53
 
52
54
  def start(self):
55
+ with self._pipe_lock:
56
+ if self._pipe_closed:
57
+ raise RuntimeError("pipe bridge is closed")
58
+ return self._start_transport()
59
+
60
+ def _start_transport(self):
53
61
  runtime = self.globals_dict.get("_managed_runtime")
54
62
  if runtime is not None:
55
63
  runtime.attach_bridge(self)
@@ -80,23 +88,31 @@ class PipeNormalQmtBridge(NormalQmtBridge):
80
88
  self._publish_runtime_report("start")
81
89
  return self
82
90
 
91
+ def stop_transport(self):
92
+ # This phase must not wait for QMT native work or dispatch_lock.
93
+ with self._pipe_lock:
94
+ self._pipe_closed = True
95
+ self.running = False
96
+ tx, self.tx = self.tx, None
97
+ event = getattr(self, "worker_event", None)
98
+ if event is not None:
99
+ event.set()
100
+ if tx is not None:
101
+ tx.close()
102
+ deadline = time.monotonic() + 0.5
103
+ for worker in (getattr(self, "recv_thread", None), getattr(self, "worker_thread", None)):
104
+ if worker is not None and worker is not threading.current_thread() and worker.is_alive():
105
+ worker.join(max(0.0, deadline - time.monotonic()))
106
+
83
107
  def close(self):
108
+ self.stop_transport()
84
109
  self._flush_pending_order_errors(force=True)
85
- self.running = False
86
- self.worker_event.set()
87
110
  self._close_quote_subscriptions()
88
111
  if self.context is not None and self.schedule_key:
89
112
  try:
90
113
  self.context.cancel_schedule_run(self.schedule_key)
91
114
  except Exception:
92
115
  pass
93
- tx = self.tx
94
- self.tx = None
95
- if tx is not None:
96
- try:
97
- tx.close()
98
- except Exception:
99
- pass
100
116
  self._log("pipe normal bridge stopped")
101
117
 
102
118
  def _status_extra(self):
@@ -138,8 +154,16 @@ class PipeTradeBridge(TxTradeBridge):
138
154
  )
139
155
  self.pipe_name = pipe_name or DEFAULT_PIPE_NAME
140
156
  self.connect_timeout_ms = int(connect_timeout_ms)
157
+ self._pipe_lock = threading.RLock()
158
+ self._pipe_closed = False
141
159
 
142
160
  def start(self):
161
+ with self._pipe_lock:
162
+ if self._pipe_closed:
163
+ raise RuntimeError("pipe bridge is closed")
164
+ return self._start_transport()
165
+
166
+ def _start_transport(self):
143
167
  runtime = self.globals_dict.get("_managed_runtime")
144
168
  if runtime is not None:
145
169
  runtime.attach_bridge(self)
@@ -166,15 +190,11 @@ class PipeTradeBridge(TxTradeBridge):
166
190
  self._publish_runtime_report("start")
167
191
  return self
168
192
 
193
+ def stop_transport(self):
194
+ PipeNormalQmtBridge.stop_transport(self)
195
+
169
196
  def close(self):
170
- self.running = False
171
- tx = self.tx
172
- self.tx = None
173
- if tx is not None:
174
- try:
175
- tx.close()
176
- except Exception:
177
- pass
197
+ self.stop_transport()
178
198
  self._log("pipe trade bridge stopped")
179
199
 
180
200
  def poll(self, max_messages=100, timeout=0):
@@ -254,12 +254,14 @@ class NamedPipeConnection(object):
254
254
  offset += written.value
255
255
 
256
256
 
257
- def connect_pipe(pipe_name=None, timeout_ms=3000):
257
+ def connect_pipe(pipe_name=None, timeout_ms=3000, stop_event=None):
258
258
  pipe_name = normalize_pipe_name(pipe_name)
259
259
  k32 = kernel32()
260
- deadline = time.time() + max(float(timeout_ms), 1.0) / 1000.0
260
+ deadline = time.monotonic() + max(float(timeout_ms), 1.0) / 1000.0
261
261
  last_error = None
262
262
  while True:
263
+ if stop_event is not None and stop_event.is_set():
264
+ raise ConnectionAbortedError("named pipe connection cancelled")
263
265
  handle = k32.dll.CreateFileW(
264
266
  pipe_name,
265
267
  GENERIC_READ | GENERIC_WRITE,
@@ -270,16 +272,23 @@ def connect_pipe(pipe_name=None, timeout_ms=3000):
270
272
  None,
271
273
  )
272
274
  if not k32.invalid_handle(handle):
273
- return NamedPipeConnection(handle, pipe_name, owner_server_side=False)
275
+ conn = NamedPipeConnection(handle, pipe_name, owner_server_side=False)
276
+ if stop_event is not None and stop_event.is_set():
277
+ conn.close()
278
+ raise ConnectionAbortedError("named pipe connection cancelled")
279
+ return conn
274
280
  last_error = k32.last_error()
275
281
  if last_error == ERROR_PIPE_BUSY:
276
- wait_ms = min(250, max(1, int((deadline - time.time()) * 1000)))
282
+ wait_ms = min(50 if stop_event is not None else 250,
283
+ max(1, int((deadline - time.monotonic()) * 1000)))
277
284
  k32.dll.WaitNamedPipeW(pipe_name, wait_ms)
278
- elif last_error in (ERROR_FILE_NOT_FOUND, ERROR_ACCESS_DENIED):
279
- time.sleep(0.05)
285
+ elif stop_event is not None:
286
+ stop_event.wait(0.05)
280
287
  else:
281
288
  time.sleep(0.05)
282
- if time.time() >= deadline:
289
+ if stop_event is not None and stop_event.is_set():
290
+ raise ConnectionAbortedError("named pipe connection cancelled")
291
+ if time.monotonic() >= deadline:
283
292
  raise OSError(last_error or 0, "connect named pipe timeout pipe=%s winerror=%s" % (pipe_name, last_error))
284
293
 
285
294
 
@@ -366,21 +375,27 @@ class PipeTxClient(object):
366
375
  self._sender = None
367
376
  self._sender_conn = None
368
377
  self._stop_event = threading.Event()
378
+ self._closed = False
379
+ self._connecting_conns = set()
369
380
  self.send_max_bytes = int(os.environ.get("CFQUANT_PIPE_SEND_MAX_BYTES", 128 * 1024 * 1024))
370
381
  self.send_timeout = float(os.environ.get("CFQUANT_PIPE_SEND_TIMEOUT", "15"))
371
382
 
372
383
  def start(self):
373
- if self.running:
374
- return self
375
- self.running = True
376
- self._stop_event.clear()
377
- self.thread = threading.Thread(target=self._connect_loop)
378
- self.thread.daemon = True
379
- self.thread.start()
380
- if self.heartbeat_interval > 0 or self.send_timeout > 0:
381
- self.heartbeat_thread = threading.Thread(target=self._heartbeat_loop)
382
- self.heartbeat_thread.daemon = True
383
- self.heartbeat_thread.start()
384
+ with self.conn_lock:
385
+ # A strategy restart creates a new client. Reusing this stop event
386
+ # could reactivate workers still exiting from the previous run.
387
+ if self._closed:
388
+ raise RuntimeError("pipe tx client is closed")
389
+ if self.running:
390
+ return self
391
+ self.running = True
392
+ self.thread = threading.Thread(target=self._connect_loop, name="cfquant-pipe-connect")
393
+ self.thread.daemon = True
394
+ self.thread.start()
395
+ if self.heartbeat_interval > 0 or self.send_timeout > 0:
396
+ self.heartbeat_thread = threading.Thread(target=self._heartbeat_loop, name="cfquant-pipe-heartbeat")
397
+ self.heartbeat_thread.daemon = True
398
+ self.heartbeat_thread.start()
384
399
  return self
385
400
 
386
401
  def start_tx(self):
@@ -406,6 +421,8 @@ class PipeTxClient(object):
406
421
  if not isinstance(payload, str):
407
422
  raise TypeError("pipe publish payload must be an encoded string")
408
423
  with self.conn_lock:
424
+ if self._closed:
425
+ return {"code": -1, "msg": "pipe tx client is closed"}
409
426
  if (self.tx_conn, self.connection_generation) != (conn, generation):
410
427
  raise RuntimeError("pipe connection changed before send")
411
428
  if self._sender is None or self._sender_conn is not conn:
@@ -430,25 +447,59 @@ class PipeTxClient(object):
430
447
  return {"code": -1, "msg": str(e)}
431
448
 
432
449
  def close(self):
433
- self.running = False
434
- self._stop_event.set()
450
+ with self.conn_lock:
451
+ self._closed = True
452
+ self.running = False
453
+ self._stop_event.set()
454
+ connecting = tuple(self._connecting_conns)
455
+ self._connecting_conns.clear()
456
+ sender = self._sender
457
+ workers = (self.thread, self.heartbeat_thread)
458
+ # Interrupt ReadFile/WriteFile, including either hello handshake,
459
+ # before joining. Native cleanup and joins must not hold conn_lock.
460
+ self._drop_pair(None, reason="pipe tx client closed", conns=connecting)
461
+ # A concurrent submit may have started the sender just before it was
462
+ # closed. Read its thread only after close prevents further submits.
463
+ workers += (getattr(sender, "_thread", None),)
435
464
  try:
436
465
  self.Q.put_nowait(None)
437
- except Exception:
466
+ except queue.Full:
438
467
  pass
439
- self._drop_pair(None, reason="pipe tx client closed", conns=self._get_conns())
468
+ deadline = time.monotonic() + 1.0
469
+ for worker in workers:
470
+ if worker is not None and worker is not threading.current_thread() and worker.is_alive():
471
+ worker.join(max(0.0, deadline - time.monotonic()))
472
+
473
+ def _open_connection(self, role):
474
+ conn = connect_pipe(self.pipe_name, timeout_ms=self.connect_timeout_ms,
475
+ stop_event=self._stop_event)
476
+ try:
477
+ with self.conn_lock:
478
+ if self._closed:
479
+ raise ConnectionAbortedError("pipe tx client is closed")
480
+ self._connecting_conns.add(conn)
481
+ conn.write_frame(dumps_pipe_message(self._pipe_envelope("hello", role)))
482
+ if self._stop_event.is_set():
483
+ raise ConnectionAbortedError("pipe tx client is closed")
484
+ return conn
485
+ except BaseException:
486
+ with self.conn_lock:
487
+ self._connecting_conns.discard(conn)
488
+ self._close_conns((conn,))
489
+ raise
440
490
 
441
491
  def _connect_loop(self):
442
- while self.running:
492
+ while self.running and not self._stop_event.is_set():
443
493
  rx_conn = None
444
494
  tx_conn = None
445
495
  generation = None
446
496
  try:
447
- rx_conn = connect_pipe(self.pipe_name, timeout_ms=self.connect_timeout_ms)
448
- rx_conn.write_frame(dumps_pipe_message(self._pipe_envelope("hello", "qmt_rx")))
449
- tx_conn = connect_pipe(self.pipe_name, timeout_ms=self.connect_timeout_ms)
450
- tx_conn.write_frame(dumps_pipe_message(self._pipe_envelope("hello", "qmt_tx")))
497
+ rx_conn = self._open_connection("qmt_rx")
498
+ tx_conn = self._open_connection("qmt_tx")
451
499
  with self.conn_lock:
500
+ if self._closed:
501
+ raise ConnectionAbortedError("pipe tx client is closed")
502
+ self._connecting_conns.difference_update((rx_conn, tx_conn))
452
503
  self.connection_generation += 1
453
504
  generation = self.connection_generation
454
505
  self.rx_conn = rx_conn
@@ -461,29 +512,44 @@ class PipeTxClient(object):
461
512
  )
462
513
  self._read_loop(rx_conn, generation)
463
514
  except Exception as e:
464
- self.last_error = str(e)
465
- if self.running:
466
- self._log("pipe connect/read failed: %s" % e)
515
+ with self.conn_lock:
516
+ if self.running and not self._closed:
517
+ self.last_error = str(e)
518
+ self._log("pipe connect/read failed: %s" % e)
467
519
  finally:
520
+ with self.conn_lock:
521
+ self._connecting_conns.difference_update((rx_conn, tx_conn))
468
522
  if generation is None:
469
523
  self._close_conns((rx_conn, tx_conn))
470
524
  else:
471
525
  self._drop_pair(generation, conns=(rx_conn, tx_conn))
472
- if self.running:
473
- time.sleep(self.reconnect_interval)
526
+ if self._stop_event.wait(max(0.0, self.reconnect_interval)):
527
+ break
474
528
 
475
529
  def _read_loop(self, conn, generation):
476
- while self.running and self._get_rx_state() == (conn, generation):
530
+ while self.running and not self._stop_event.is_set() and self._get_rx_state() == (conn, generation):
477
531
  raw = conn.read_frame()
478
532
  if raw is None:
479
533
  break
480
534
  envelope = loads_pipe_message(raw)
481
535
  if envelope:
482
536
  payload = envelope.get("payload")
483
- if payload:
484
- self.Q.put(payload)
537
+ if payload and not self._queue_received(payload):
538
+ return
485
539
  continue
486
- self.Q.put(raw)
540
+ if not self._queue_received(raw):
541
+ return
542
+
543
+ def _queue_received(self, payload):
544
+ # A stopped bridge no longer drains Q. Never leave the reader blocked
545
+ # forever in put() when its bounded queue is full at shutdown.
546
+ while self.running and not self._stop_event.is_set():
547
+ try:
548
+ self.Q.put(payload, timeout=0.1)
549
+ return True
550
+ except queue.Full:
551
+ pass
552
+ return False
487
553
 
488
554
  def _get_rx_conn(self):
489
555
  with self.conn_lock:
@@ -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: