cfquant 0.2.53__tar.gz → 0.2.54__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 (201) hide show
  1. {cfquant-0.2.53 → cfquant-0.2.54}/MANIFEST.in +1 -0
  2. {cfquant-0.2.53 → cfquant-0.2.54}/PKG-INFO +3 -1
  3. cfquant-0.2.54/cfquant/qmt_component_agent.js +249 -0
  4. cfquant-0.2.54/cfquant/qmt_component_login.py +310 -0
  5. cfquant-0.2.54/cfquant/qmt_credentials.py +72 -0
  6. {cfquant-0.2.53 → cfquant-0.2.54}/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 +1 -1
  7. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_lite_deployment_paths.py +1 -0
  8. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_management.py +7 -2
  9. cfquant-0.2.54/cfquant/tests/test_qmt_auto_login_browser.py +175 -0
  10. cfquant-0.2.54/cfquant/tests/test_qmt_auto_login_integration.py +356 -0
  11. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_qmt_binding_settings.py +2 -2
  12. cfquant-0.2.54/cfquant/tests/test_qmt_component_login.py +165 -0
  13. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_qmt_strategy_browser.py +15 -6
  14. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_qmt_strategy_web.py +44 -34
  15. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_setup_qmt_prompt.py +1 -0
  16. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_stock_connect.py +1 -1
  17. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/trading_calendar.py +8 -0
  18. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/version.py +2 -2
  19. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant.egg-info/PKG-INFO +3 -1
  20. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant.egg-info/SOURCES.txt +6 -0
  21. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant.egg-info/requires.txt +4 -0
  22. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant_web_server.py +272 -98
  23. {cfquant-0.2.53 → cfquant-0.2.54}/pyproject.toml +4 -1
  24. {cfquant-0.2.53 → cfquant-0.2.54}/qmt_scripts/CFQUANT_LITE.py +1 -1
  25. {cfquant-0.2.53 → cfquant-0.2.54}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_LITE_SH.py +1 -1
  26. {cfquant-0.2.53 → cfquant-0.2.54}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_LITE_SZ.py +1 -1
  27. {cfquant-0.2.53 → cfquant-0.2.54}/requirements.txt +2 -0
  28. {cfquant-0.2.53 → cfquant-0.2.54}/web_dashboard/app.js +247 -15
  29. {cfquant-0.2.53 → cfquant-0.2.54}/web_dashboard/index.html +49 -35
  30. {cfquant-0.2.53 → cfquant-0.2.54}/web_dashboard/styles.css +20 -1
  31. {cfquant-0.2.53 → cfquant-0.2.54}/LICENSE +0 -0
  32. {cfquant-0.2.53 → cfquant-0.2.54}/LTtx/__init__.py +0 -0
  33. {cfquant-0.2.53 → cfquant-0.2.54}/LTtx/tx/Config.txt +0 -0
  34. {cfquant-0.2.53 → cfquant-0.2.54}/LTtx/tx/LTtx_server.py +0 -0
  35. {cfquant-0.2.53 → cfquant-0.2.54}/LTtx/tx/__init__.py +0 -0
  36. {cfquant-0.2.53 → cfquant-0.2.54}/LTtx/tx/tx.py +0 -0
  37. {cfquant-0.2.53 → cfquant-0.2.54}/README.md +0 -0
  38. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/__init__.py +0 -0
  39. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/_editable_install.py +0 -0
  40. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/account_routing.py +0 -0
  41. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/batch_orders.py +0 -0
  42. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/build_info.py +0 -0
  43. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/callback_dispatcher.py +0 -0
  44. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/cftrader.py +0 -0
  45. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/channels.py +0 -0
  46. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/cli.py +0 -0
  47. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/client.py +0 -0
  48. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/config.py +0 -0
  49. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/diagnostics.py +0 -0
  50. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/flow_control.py +0 -0
  51. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/hub.py +0 -0
  52. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/level2.py +0 -0
  53. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/log_management.py +0 -0
  54. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/logging_i18n.py +0 -0
  55. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/management.py +0 -0
  56. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/normal_bridge.py +0 -0
  57. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/order_identity.py +0 -0
  58. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/order_meta.py +0 -0
  59. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/pipe_bridge.py +0 -0
  60. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/pipe_client.py +0 -0
  61. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/pipe_hub.py +0 -0
  62. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/pipe_transport.py +0 -0
  63. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/protocol.py +0 -0
  64. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/qmt_binding_settings.py +0 -0
  65. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/qmt_bridge.py +0 -0
  66. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/qmt_pipe_runtime.py +0 -0
  67. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/qmt_strategy_deploy.py +0 -0
  68. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/qmt_strategy_package.py +0 -0
  69. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/qmt_strategy_runtime.py +0 -0
  70. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/quote_history.py +0 -0
  71. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/rpc_timing.py +0 -0
  72. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/runtime_report.py +0 -0
  73. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/stock_connect.py +0 -0
  74. {cfquant-0.2.53 → cfquant-0.2.54}/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
  75. {cfquant-0.2.53 → cfquant-0.2.54}/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
  76. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/12_/350/256/242/345/215/225/345/244/207/346/263/250/346/265/213/350/257/225.py" +0 -0
  77. {cfquant-0.2.53 → cfquant-0.2.54}/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
  78. {cfquant-0.2.53 → cfquant-0.2.54}/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
  79. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/16_/345/220/257/345/212/250/345/256/211/350/243/205/346/265/213/350/257/225.py" +0 -0
  80. {cfquant-0.2.53 → cfquant-0.2.54}/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
  81. {cfquant-0.2.53 → cfquant-0.2.54}/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
  82. {cfquant-0.2.53 → cfquant-0.2.54}/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
  83. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/1_/350/241/214/346/203/205/346/216/245/346/224/266/346/265/213/350/257/225.py" +0 -0
  84. {cfquant-0.2.53 → cfquant-0.2.54}/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
  85. {cfquant-0.2.53 → cfquant-0.2.54}/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
  86. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/22_/350/277/236/346/216/245/350/257/212/346/226/255/346/265/213/350/257/225.py" +0 -0
  87. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/23_/347/244/272/344/276/213/351/205/215/347/275/256/346/265/213/350/257/225.py" +0 -0
  88. {cfquant-0.2.53 → cfquant-0.2.54}/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
  89. {cfquant-0.2.53 → cfquant-0.2.54}/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
  90. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/26_/345/205/250/346/216/250/350/256/242/351/230/205/346/265/213/350/257/225.py" +0 -0
  91. {cfquant-0.2.53 → cfquant-0.2.54}/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
  92. {cfquant-0.2.53 → cfquant-0.2.54}/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
  93. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/29_Level2/350/241/214/346/203/205/346/265/213/350/257/225.py" +0 -0
  94. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/2_/346/225/260/346/215/256/350/216/267/345/217/226/346/265/213/350/257/225.py" +0 -0
  95. {cfquant-0.2.53 → cfquant-0.2.54}/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
  96. {cfquant-0.2.53 → cfquant-0.2.54}/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
  97. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/32_/345/274/202/346/255/245/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  98. {cfquant-0.2.53 → cfquant-0.2.54}/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
  99. {cfquant-0.2.53 → cfquant-0.2.54}/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
  100. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/3_/346/225/260/346/215/256/344/270/213/350/275/275/346/265/213/350/257/225.py" +0 -0
  101. {cfquant-0.2.53 → cfquant-0.2.54}/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
  102. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/5_/347/234/237/345/256/236/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  103. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/6_/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
  104. {cfquant-0.2.53 → cfquant-0.2.54}/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
  105. {cfquant-0.2.53 → cfquant-0.2.54}/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
  106. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/9_/345/217/257/346/222/244/350/256/242/345/215/225/346/265/213/350/257/225.py" +0 -0
  107. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/_helpers.py +0 -0
  108. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/log/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_20260911_024617.md" +0 -0
  109. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/log/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_20260921_175732.md" +0 -0
  110. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/readme.md +0 -0
  111. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_advanced_parity.py +0 -0
  112. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_api_tester_browser.py +0 -0
  113. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_api_tester_web.py +0 -0
  114. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
  115. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_binding_runtime_browser.py +0 -0
  116. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_callback_requests.py +0 -0
  117. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_cftrader.py +0 -0
  118. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_cftrader_tutorial.py +0 -0
  119. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_cftrader_web.py +0 -0
  120. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_credit_compat.py +0 -0
  121. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_dataframe_encoding.py +0 -0
  122. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_json_serialization.py +0 -0
  123. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_latency_flow_control.py +0 -0
  124. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_lite_account_routing.py +0 -0
  125. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_lite_parity.py +0 -0
  126. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_lite_qmt_dispatch.py +0 -0
  127. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_local_trading_calendar.py +0 -0
  128. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_log_management.py +0 -0
  129. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_management_batch.py +0 -0
  130. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_market_data_fallback.py +0 -0
  131. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_market_timing.py +0 -0
  132. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_order_identity.py +0 -0
  133. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_order_time_callback.py +0 -0
  134. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_project_updater.py +0 -0
  135. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_python_api_reference.py +0 -0
  136. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_qmt_fileless.py +0 -0
  137. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_qmt_strategy_deploy.py +0 -0
  138. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_quote_latency.py +0 -0
  139. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_quote_latency_encoding.py +0 -0
  140. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_startup_recovery.py +0 -0
  141. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_stock_connect_browser.py +0 -0
  142. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_table_conversion.py +0 -0
  143. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_trade_strategy_callback.py +0 -0
  144. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_trading_dates.py +0 -0
  145. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_tutorial_reader.py +0 -0
  146. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_tx_auto_import.py +0 -0
  147. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tests/test_tx_shutdown.py +0 -0
  148. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tx.py +0 -0
  149. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/tx_trade_bridge.py +0 -0
  150. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/xtconstant.py +0 -0
  151. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/xtdata.py +0 -0
  152. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/xttrader.py +0 -0
  153. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant/xttype.py +0 -0
  154. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant.egg-info/dependency_links.txt +0 -0
  155. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant.egg-info/entry_points.txt +0 -0
  156. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant.egg-info/top_level.txt +0 -0
  157. {cfquant-0.2.53 → cfquant-0.2.54}/cfquant_pipe_hub.py +0 -0
  158. {cfquant-0.2.53 → cfquant-0.2.54}/qmt_scripts/CFQUANT.py +0 -0
  159. {cfquant-0.2.53 → cfquant-0.2.54}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +0 -0
  160. {cfquant-0.2.53 → cfquant-0.2.54}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
  161. {cfquant-0.2.53 → cfquant-0.2.54}/qmt_scripts/__init__.py +0 -0
  162. {cfquant-0.2.53 → cfquant-0.2.54}/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
  163. {cfquant-0.2.53 → cfquant-0.2.54}/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
  164. {cfquant-0.2.53 → cfquant-0.2.54}/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
  165. {cfquant-0.2.53 → cfquant-0.2.54}/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
  166. {cfquant-0.2.53 → cfquant-0.2.54}/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
  167. {cfquant-0.2.53 → cfquant-0.2.54}/setup.cfg +0 -0
  168. {cfquant-0.2.53 → cfquant-0.2.54}/web_dashboard/__init__.py +0 -0
  169. {cfquant-0.2.53 → cfquant-0.2.54}/web_dashboard/api-tester.js +0 -0
  170. {cfquant-0.2.53 → cfquant-0.2.54}/web_dashboard/avatars/amber-pulse.svg +0 -0
  171. {cfquant-0.2.53 → cfquant-0.2.54}/web_dashboard/avatars/copper-grid.svg +0 -0
  172. {cfquant-0.2.53 → cfquant-0.2.54}/web_dashboard/avatars/market-blue.svg +0 -0
  173. {cfquant-0.2.53 → cfquant-0.2.54}/web_dashboard/avatars/rose-circuit.svg +0 -0
  174. {cfquant-0.2.53 → cfquant-0.2.54}/web_dashboard/avatars/signal-green.svg +0 -0
  175. {cfquant-0.2.53 → cfquant-0.2.54}/web_dashboard/avatars/slate-wave.svg +0 -0
  176. {cfquant-0.2.53 → cfquant-0.2.54}/web_dashboard/avatars/teal-orbit.svg +0 -0
  177. {cfquant-0.2.53 → cfquant-0.2.54}/web_dashboard/avatars/violet-node.svg +0 -0
  178. {cfquant-0.2.53 → cfquant-0.2.54}/web_dashboard/cftrader-api-data.js +0 -0
  179. {cfquant-0.2.53 → cfquant-0.2.54}/web_dashboard/docs.js +0 -0
  180. {cfquant-0.2.53 → cfquant-0.2.54}/web_dashboard/favicon.svg +0 -0
  181. {cfquant-0.2.53 → cfquant-0.2.54}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
  182. {cfquant-0.2.53 → cfquant-0.2.54}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
  183. {cfquant-0.2.53 → cfquant-0.2.54}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
  184. {cfquant-0.2.53 → cfquant-0.2.54}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
  185. {cfquant-0.2.53 → cfquant-0.2.54}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
  186. {cfquant-0.2.53 → cfquant-0.2.54}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
  187. {cfquant-0.2.53 → cfquant-0.2.54}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
  188. {cfquant-0.2.53 → cfquant-0.2.54}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
  189. {cfquant-0.2.53 → cfquant-0.2.54}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
  190. {cfquant-0.2.53 → cfquant-0.2.54}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
  191. {cfquant-0.2.53 → cfquant-0.2.54}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
  192. {cfquant-0.2.53 → cfquant-0.2.54}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
  193. {cfquant-0.2.53 → cfquant-0.2.54}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
  194. {cfquant-0.2.53 → cfquant-0.2.54}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
  195. {cfquant-0.2.53 → cfquant-0.2.54}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
  196. {cfquant-0.2.53 → cfquant-0.2.54}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
  197. {cfquant-0.2.53 → cfquant-0.2.54}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
  198. {cfquant-0.2.53 → cfquant-0.2.54}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
  199. {cfquant-0.2.53 → cfquant-0.2.54}/web_dashboard/python-api-data.js +0 -0
  200. {cfquant-0.2.53 → cfquant-0.2.54}/web_dashboard/python-api.js +0 -0
  201. {cfquant-0.2.53 → cfquant-0.2.54}/web_dashboard/vendor/mermaid.min.js +0 -0
@@ -4,6 +4,7 @@ include cfquant_web_server.py
4
4
  include cfquant_pipe_hub.py
5
5
 
6
6
  recursive-include cfquant *.py
7
+ include cfquant/qmt_component_agent.js
7
8
  recursive-include cfquant/tests *.py *.md
8
9
  recursive-include LTtx *.py
9
10
  include LTtx/tx/Config.txt
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cfquant
3
- Version: 0.2.53
3
+ Version: 0.2.54
4
4
  Summary: QMT local bridge and xtquant-compatible Python client.
5
5
  License: MIT
6
6
  Requires-Python: >=3.8
@@ -14,6 +14,8 @@ Requires-Dist: pytz
14
14
  Requires-Dist: tabulate
15
15
  Requires-Dist: pandas-market-calendars
16
16
  Requires-Dist: importlib-resources>=5
17
+ Requires-Dist: frida<18,>=17.0; sys_platform == "win32"
18
+ Requires-Dist: pywin32>=306; sys_platform == "win32"
17
19
  Provides-Extra: dev
18
20
  Requires-Dist: build>=1.0; extra == "dev"
19
21
  Requires-Dist: twine>=5.0; extra == "dev"
@@ -0,0 +1,249 @@
1
+ 'use strict';
2
+
3
+ const core = Process.getModuleByName('Qt5Core.dll');
4
+ const widgets = Process.getModuleByName('Qt5Widgets.dll');
5
+ const user32 = Process.getModuleByName('user32.dll');
6
+ const fn = (mod, name, ret, args) => new NativeFunction(mod.getExportByName(name), ret, args);
7
+ const findWidget = fn(widgets, '?find@QWidget@@SAPEAV1@_K@Z', 'pointer', ['uint64']);
8
+ const children = fn(core, '?children@QObject@@QEBAAEBV?$QList@PEAVQObject@@@@XZ', 'pointer', ['pointer']);
9
+ const inherits = fn(core, '?inherits@QObject@@QEBA_NPEBD@Z', 'bool', ['pointer', 'pointer']);
10
+ const className = fn(core, '?className@QMetaObject@@QEBAPEBDXZ', 'pointer', ['pointer']);
11
+ const isVisible = fn(widgets, '?isVisible@QWidget@@QEBA_NXZ', 'bool', ['pointer']);
12
+ const isEnabled = fn(widgets, '?isEnabled@QWidget@@QEBA_NXZ', 'bool', ['pointer']);
13
+ const echoMode = fn(widgets, '?echoMode@QLineEdit@@QEBA?AW4EchoMode@1@XZ', 'int', ['pointer']);
14
+ const setText = fn(widgets, '?setText@QLineEdit@@QEAAXAEBVQString@@@Z', 'void', ['pointer', 'pointer']);
15
+ const stringCtor = fn(core, '??0QString@@QEAA@PEBVQChar@@H@Z', 'pointer', ['pointer', 'pointer', 'int']);
16
+ const stringDtor = fn(core, '??1QString@@QEAA@XZ', 'void', ['pointer']);
17
+ const stringSize = fn(core, '?size@QString@@QEBAHXZ', 'int', ['pointer']);
18
+ const stringData = fn(core, '?utf16@QString@@QEBAPEBGXZ', 'pointer', ['pointer']);
19
+ const buttonText = fn(widgets, '?text@QAbstractButton@@QEBA?AVQString@@XZ', 'pointer', ['pointer', 'pointer']);
20
+ const editText = fn(widgets, '?text@QLineEdit@@QEBA?AVQString@@XZ', 'pointer', ['pointer', 'pointer']);
21
+ const labelText = fn(widgets, '?text@QLabel@@QEBA?AVQString@@XZ', 'pointer', ['pointer', 'pointer']);
22
+ const buttonClick = fn(widgets, '?click@QAbstractButton@@QEAAXXZ', 'void', ['pointer']);
23
+ const closeWidget = fn(widgets, '?close@QWidget@@QEAA_NXZ', 'bool', ['pointer']);
24
+ const comboEditable = fn(widgets, '?isEditable@QComboBox@@QEBA_NXZ', 'bool', ['pointer']);
25
+ const comboSetText = fn(widgets, '?setEditText@QComboBox@@QEAAXAEBVQString@@@Z', 'void', ['pointer', 'pointer']);
26
+ const getThread = fn(user32, 'GetWindowThreadProcessId', 'uint32', ['pointer', 'pointer']);
27
+ const postMessage = fn(user32, 'PostMessageW', 'bool', ['pointer', 'uint32', 'pointer', 'pointer']);
28
+ const typeNames = new Map();
29
+ let pending = null;
30
+
31
+ function hasType(object, name) {
32
+ if (!typeNames.has(name)) typeNames.set(name, Memory.allocUtf8String(name));
33
+ return inherits(object, typeNames.get(name));
34
+ }
35
+
36
+ function typeName(object) {
37
+ const metaFn = new NativeFunction(object.readPointer().readPointer(), 'pointer', ['pointer']);
38
+ return className(metaFn(object)).readUtf8String();
39
+ }
40
+
41
+ function childObjects(object) {
42
+ const d = children(object).readPointer();
43
+ const begin = d.add(8).readS32(), end = d.add(12).readS32();
44
+ if (begin < 0 || end < begin || end - begin > 5000) throw new Error('Unexpected QList layout');
45
+ const result = [];
46
+ for (let i = begin; i < end; i++) result.push(d.add(16 + i * Process.pointerSize).readPointer());
47
+ return result;
48
+ }
49
+
50
+ function label(object) {
51
+ const result = Memory.alloc(Process.pointerSize);
52
+ buttonText(object, result);
53
+ try { return stringData(result).readUtf16String(stringSize(result)); }
54
+ finally { stringDtor(result); }
55
+ }
56
+
57
+ function walk(root) {
58
+ const result = [], queue = [{object: root, parent: null}], seen = new Set();
59
+ while (queue.length) {
60
+ const item = queue.shift(), object = item.object;
61
+ if (object.isNull() || seen.has(object.toString())) continue;
62
+ seen.add(object.toString());
63
+ if (seen.size > 5000) throw new Error('Object tree too large');
64
+ const isWidget = hasType(object, 'QWidget');
65
+ const row = {address: object.toString(), parent: item.parent, class_name: typeName(object)};
66
+ if (isWidget) {
67
+ row.visible = isVisible(object);
68
+ row.enabled = isEnabled(object);
69
+ if (hasType(object, 'QLineEdit')) row.echo_mode = echoMode(object);
70
+ if (hasType(object, 'QAbstractButton')) row.label = label(object);
71
+ }
72
+ result.push(row);
73
+ childObjects(object).forEach(child => queue.push({object: child, parent: object.toString()}));
74
+ }
75
+ return result;
76
+ }
77
+
78
+ function getRoot(hwnd) {
79
+ const root = findWidget(uint64(hwnd));
80
+ if (root.isNull()) throw new Error('No Qt widget for target window');
81
+ return root;
82
+ }
83
+
84
+ function passwordEditor(root, requireEnabled = true) {
85
+ if (!hasType(root, 'TC::CLoginDialog')) throw new Error('Not a QMT login dialog');
86
+ const rows = walk(root);
87
+ const edits = rows.filter(row => row.echo_mode === 2 && row.visible && (!requireEnabled || row.enabled));
88
+ if (edits.length !== 1) throw new Error('Expected exactly one visible password editor');
89
+ return {rows, editor: ptr(edits[0].address)};
90
+ }
91
+
92
+ function hasText(editor) {
93
+ const value = Memory.alloc(Process.pointerSize);
94
+ editText(editor, value);
95
+ // Only return emptiness; never copy a remembered password out of the process.
96
+ try { return stringSize(value) > 0; }
97
+ finally { stringDtor(value); }
98
+ }
99
+
100
+ function noticeKind(rows) {
101
+ let credential = false, network = false;
102
+ for (const row of rows) {
103
+ const object = ptr(row.address);
104
+ if (!row.visible || !hasType(object, 'QLabel')) continue;
105
+ const value = Memory.alloc(Process.pointerSize);
106
+ labelText(object, value);
107
+ let text;
108
+ try { text = stringData(value).readUtf16String(stringSize(value)); }
109
+ finally { stringDtor(value); }
110
+ if (/验证码|动态口令|短信验证|captcha/i.test(text)) return 'challenge';
111
+ if (/密码.{0,12}(错误|不正确|失败|无效)|口令.{0,12}(错误|不正确|失败)|password.{0,16}(incorrect|invalid|wrong)/i.test(text)) credential = true;
112
+ if (/(网络|连接|服务器).{0,16}(失败|超时|异常)|network.{0,16}(error|timeout)/i.test(text)) network = true;
113
+ }
114
+ return credential ? 'credential_error' : network ? 'network_error' : null;
115
+ }
116
+
117
+ function submit(root) {
118
+ passwordEditor(root);
119
+ const login = fn(Process.getModuleByName('LoginDialog.dll'),
120
+ '?slot_loginClicked@CLoginDialog@TC@@QEAAXXZ', 'void', ['pointer']);
121
+ login(root);
122
+ }
123
+
124
+ function loginIdentity(root) {
125
+ const getName = fn(Process.getModuleByName('LoginDialog.dll'),
126
+ '?getUserName@CLoginDialog@TC@@IEAA?AVQString@@XZ', 'pointer', ['pointer', 'pointer']);
127
+ const value = Memory.alloc(Process.pointerSize);
128
+ getName(root, value);
129
+ try { return stringData(value).readUtf16String(stringSize(value)); }
130
+ finally { stringDtor(value); }
131
+ }
132
+
133
+ function onGui(hwnd, operation) {
134
+ if (pending) throw new Error('An operation is already pending');
135
+ const thread = getThread(ptr(hwnd), NULL);
136
+ if (!thread) throw new Error('Target window no longer exists');
137
+ return new Promise((resolve, reject) => {
138
+ const timer = setTimeout(() => {
139
+ if (pending && pending.resolve === resolve) {
140
+ pending = null;
141
+ reject(new Error('GUI operation timeout'));
142
+ }
143
+ }, 8000);
144
+ pending = {thread, operation, resolve, reject, timer};
145
+ // Wake the normal message loop without mouse/keyboard events or foreground changes.
146
+ postMessage(ptr(hwnd), 0, NULL, NULL);
147
+ });
148
+ }
149
+
150
+ Interceptor.attach(user32.getExportByName('DispatchMessageW'), {
151
+ onLeave() {
152
+ if (!pending || this.threadId !== pending.thread) return;
153
+ const job = pending;
154
+ pending = null;
155
+ clearTimeout(job.timer);
156
+ try { job.resolve({gui_thread: this.threadId, result: job.operation()}); }
157
+ catch (error) { job.reject(error); }
158
+ }
159
+ });
160
+
161
+ rpc.exports = {
162
+ selectaccount(hwnd, account, replace = false) {
163
+ return onGui(hwnd, () => {
164
+ const root = getRoot(hwnd);
165
+ passwordEditor(root);
166
+ if (!replace && loginIdentity(root) === account) return {changed: false};
167
+ const candidates = walk(root).filter(row => row.visible && row.enabled &&
168
+ hasType(ptr(row.address), 'QComboBox') && comboEditable(ptr(row.address)));
169
+ if (candidates.length !== 1) throw new Error('No unique editable account selector');
170
+ if (replace) {
171
+ const empty = Memory.alloc(Process.pointerSize);
172
+ stringCtor(empty, Memory.allocUtf16String(''), 0);
173
+ try { comboSetText(ptr(candidates[0].address), empty); }
174
+ finally { stringDtor(empty); }
175
+ }
176
+ const data = Memory.allocUtf16String(account), value = Memory.alloc(Process.pointerSize);
177
+ stringCtor(value, data, account.length);
178
+ try { comboSetText(ptr(candidates[0].address), value); }
179
+ finally { stringDtor(value); }
180
+ if (loginIdentity(root) !== account) throw new Error('Could not select configured login account');
181
+ return {changed: true};
182
+ });
183
+ },
184
+ inspect(hwnd) { return onGui(hwnd, () => walk(getRoot(hwnd))); },
185
+ status(hwnd) {
186
+ return onGui(hwnd, () => {
187
+ const root = getRoot(hwnd);
188
+ if (!hasType(root, 'TC::CLoginDialog')) return {is_login: false, is_main: hasType(root, 'TC::CMainWindow')};
189
+ const rows = walk(root);
190
+ const edits = rows.filter(row => row.echo_mode === 2 && row.visible);
191
+ if (edits.length > 1) throw new Error('Multiple password editors are visible');
192
+ return {is_login: true, has_password: edits.length === 1 && hasText(ptr(edits[0].address)),
193
+ busy: edits.length === 0, login_identity: loginIdentity(root), notice: noticeKind(rows),
194
+ login_enabled: rows.some(row => row.visible && row.enabled && /^登\s*录(?:[((]\d+[))])?$/.test(row.label || ''))};
195
+ });
196
+ },
197
+ notice(hwnd) {
198
+ return onGui(hwnd, () => {
199
+ const rows = walk(getRoot(hwnd));
200
+ return {kind: noticeKind(rows)};
201
+ });
202
+ },
203
+ acknowledge(hwnd) {
204
+ return onGui(hwnd, () => {
205
+ const rows = walk(getRoot(hwnd));
206
+ if (noticeKind(rows) !== 'credential_error') throw new Error('Not a recognized credential failure');
207
+ const buttons = rows.filter(row => row.visible && row.enabled && /^(确定|确认|OK|&OK)$/.test(row.label || ''));
208
+ if (buttons.length !== 1) throw new Error('No unique acknowledgement button');
209
+ buttonClick(ptr(buttons[0].address));
210
+ return {acknowledged: true};
211
+ });
212
+ },
213
+ submit(hwnd) { return onGui(hwnd, () => { submit(getRoot(hwnd)); return {submitted: true}; }); },
214
+ close(hwnd) { return onGui(hwnd, () => ({closed: closeWidget(getRoot(hwnd))})); },
215
+ clicklabel(hwnd, text) {
216
+ return onGui(hwnd, () => {
217
+ const rows = walk(getRoot(hwnd)).filter(row => row.label === text && row.visible && row.enabled);
218
+ if (rows.length !== 1) throw new Error('Expected one matching enabled button');
219
+ buttonClick(ptr(rows[0].address));
220
+ return {clicked: true};
221
+ });
222
+ },
223
+ fill(hwnd, value, submit) {
224
+ return onGui(hwnd, () => {
225
+ const root = getRoot(hwnd);
226
+ if (!hasType(root, 'TC::CLoginDialog')) throw new Error('Not a QMT login dialog');
227
+ const rows = walk(root);
228
+ const edits = rows.filter(row => row.echo_mode === 2 && row.visible && row.enabled);
229
+ if (edits.length !== 1) throw new Error('Expected exactly one visible password editor');
230
+ const text = Memory.allocUtf16String(value);
231
+ const string = Memory.alloc(Process.pointerSize);
232
+ stringCtor(string, text, value.length);
233
+ try { setText(ptr(edits[0].address), string); }
234
+ finally {
235
+ stringDtor(string);
236
+ text.writeByteArray(new Uint8Array((value.length + 1) * 2));
237
+ }
238
+ const count = value.length;
239
+ value = null;
240
+ if (submit) {
241
+ const login = fn(Process.getModuleByName('LoginDialog.dll'),
242
+ '?slot_loginClicked@CLoginDialog@TC@@QEAAXXZ', 'void', ['pointer']);
243
+ login(root);
244
+ }
245
+ return {filled: true, submitted: Boolean(submit), characters: count,
246
+ input_method: 'QLineEdit::setText', login_method: submit ? 'CLoginDialog::slot_loginClicked' : null};
247
+ });
248
+ }
249
+ };
@@ -0,0 +1,310 @@
1
+ """Explicit QMT component login for a configured binding (Windows, Qt 5.6.2 x64).
2
+
3
+ Run with --binding NAME. Password is requested only if needed, using --prompt
4
+ or stdin. This tool never starts/stops QMT; it replaces the login form contents.
5
+ The web auto-start service also uses login_running_qmt after starting a process.
6
+ """
7
+ import argparse
8
+ import getpass
9
+ import json
10
+ from pathlib import Path
11
+ import sys
12
+ import time
13
+
14
+
15
+ class QmtNotReady(RuntimeError):
16
+ pass
17
+
18
+
19
+ def verify_environment(directory):
20
+ """Validate dependencies and native ABI before stopping a running terminal."""
21
+ import frida # noqa: F401
22
+ import win32api
23
+ import win32crypt # noqa: F401
24
+ directory = Path(directory)
25
+ for module in ("Qt5Core.dll", "Qt5Widgets.dll"):
26
+ version = win32api.GetFileVersionInfo(str(directory / module), "\\")
27
+ if (version["FileVersionMS"], version["FileVersionLS"]) != ((5 << 16) | 6, 2 << 16):
28
+ raise RuntimeError("QMT 组件自动登录当前仅支持 Qt 5.6.2,请先适配此客户端版本")
29
+ with (directory / "XtItClient.exe").open("rb") as stream:
30
+ stream.seek(0x3C)
31
+ offset = int.from_bytes(stream.read(4), "little")
32
+ stream.seek(offset + 4)
33
+ if int.from_bytes(stream.read(2), "little") != 0x8664:
34
+ raise RuntimeError("QMT 组件自动登录需要 64 位客户端")
35
+
36
+
37
+ def auto_login(backend, password_provider):
38
+ """Replace existing password contents and submit the supplied credential once."""
39
+ attempts = []
40
+ password_requested = False
41
+
42
+ def result(outcome):
43
+ return {"outcome": outcome, "login_succeeded": outcome in ("logged_in", "already_logged_in"),
44
+ "attempts": attempts, "password_requested": password_requested}
45
+
46
+ if backend.already_logged_in():
47
+ return result("already_logged_in")
48
+ state = backend.state()
49
+ if state.get("busy"):
50
+ return result("login_not_ready")
51
+ if state.get("notice") in ("challenge", "network_error"):
52
+ return result(state["notice"])
53
+ if state.get("notice") == "credential_error":
54
+ backend.acknowledge_failure()
55
+ password_requested = True
56
+ password = password_provider()
57
+ if not password:
58
+ return result("password_required")
59
+ try:
60
+ backend.fill(password)
61
+ finally:
62
+ password = None
63
+ if not backend.state()["login_enabled"]:
64
+ return result("login_not_ready")
65
+ backend.submit()
66
+ attempts.append("provided")
67
+ return result(backend.wait_outcome())
68
+
69
+
70
+ def select_binding(config_path, name):
71
+ config = json.loads(Path(config_path).read_text(encoding="utf-8"))
72
+ rows = config.get("account_configs") or {}
73
+ rows = rows.values() if isinstance(rows, dict) else rows
74
+ matches = [row for row in rows if row.get("display_name") == name]
75
+ if len(matches) != 1:
76
+ raise ValueError("Expected exactly one binding with that display name")
77
+ root = Path(matches[0]["qmt_dir"]).resolve()
78
+ for directory in (root, root / "bin.x64"):
79
+ if (directory / "XtItClient.exe").is_file():
80
+ return matches[0], directory
81
+ raise ValueError("Binding directory does not contain XtItClient.exe")
82
+
83
+
84
+ class QtLoginBackend:
85
+ def __init__(self, directory, timeout=40, expected_pid=None):
86
+ import frida
87
+ import psutil
88
+ import win32api
89
+ import win32event
90
+ import win32gui
91
+ import win32process
92
+ self.gui, self.process = win32gui, win32process
93
+ self.timeout = timeout
94
+ self.hwnd = None
95
+ self.identity = None
96
+ self.failure_hwnd = None
97
+ self.session = None
98
+ self.mutex = None
99
+ self.messages = []
100
+ self.foreground_before = win32gui.GetForegroundWindow()
101
+ expected = (directory / "XtItClient.exe").resolve()
102
+ matches = []
103
+ for process in psutil.process_iter(["name"]):
104
+ try:
105
+ if (process.info["name"] or "").lower() == "xtitclient.exe" and Path(process.exe()).resolve() == expected:
106
+ matches.append(process)
107
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
108
+ continue
109
+ if not matches:
110
+ raise QmtNotReady("QMT process is not ready")
111
+ if len(matches) != 1:
112
+ raise RuntimeError("Expected exactly one running QMT for this binding directory")
113
+ self.pid = matches[0].pid
114
+ if expected_pid is not None and self.pid != expected_pid:
115
+ raise RuntimeError("QMT process changed; stopping this login attempt")
116
+ verify_environment(directory)
117
+ self.mutex = win32event.CreateMutex(None, False, "Local\\CFQuantQmtComponentLogin_" + str(self.pid))
118
+ acquired = win32event.WaitForSingleObject(self.mutex, 0)
119
+ if acquired not in (0, 0x80):
120
+ win32api.CloseHandle(self.mutex)
121
+ self.mutex = None
122
+ raise RuntimeError("Another component login is already running for this QMT")
123
+ try:
124
+ self.session = frida.attach(self.pid)
125
+ readiness = self.session.create_script("rpc.exports.ready = () => ['Qt5Core.dll', 'Qt5Widgets.dll', 'LoginDialog.dll'].every(n => Process.findModuleByName(n) !== null);")
126
+ readiness.load()
127
+ ready = readiness.exports_sync.ready()
128
+ readiness.unload()
129
+ if not ready:
130
+ raise QmtNotReady("QMT login modules are still loading")
131
+ script = self.session.create_script(Path(__file__).with_name("qmt_component_agent.js").read_text(encoding="utf-8"))
132
+ script.on("message", lambda message, data: self.messages.append("agent_error") if message.get("type") == "error" else None)
133
+ script.load()
134
+ self.script, self.api = script, script.exports_sync
135
+ self.existing_main = False
136
+ self.main_hwnd = None
137
+ for hwnd in self.windows():
138
+ state = self.api.status(str(hwnd))["result"]
139
+ if state.get("is_login"):
140
+ if self.hwnd is not None:
141
+ raise RuntimeError("Multiple login dialogs found")
142
+ self.hwnd = hwnd
143
+ self.identity = state["login_identity"]
144
+ elif state.get("is_main"):
145
+ self.existing_main = True
146
+ self.main_hwnd = hwnd
147
+ if self.hwnd is None and not self.existing_main:
148
+ raise QmtNotReady("No supported login or main window found yet")
149
+ self.minimized_before = bool(self.hwnd and self.gui.IsIconic(self.hwnd))
150
+ except Exception:
151
+ self.close()
152
+ raise
153
+
154
+ def windows(self):
155
+ result = []
156
+ def visit(hwnd, _):
157
+ if self.process.GetWindowThreadProcessId(hwnd)[1] == self.pid and self.gui.IsWindowVisible(hwnd) and self.gui.GetClassName(hwnd).startswith("Qt"):
158
+ result.append(hwnd)
159
+ self.gui.EnumWindows(visit, None)
160
+ return result
161
+
162
+ def already_logged_in(self):
163
+ return self.existing_main and self.hwnd is None
164
+
165
+ def state(self):
166
+ state = self.api.status(str(self.hwnd))["result"]
167
+ if not state.get("is_login") or not self.identity or state.get("login_identity") != self.identity:
168
+ raise RuntimeError("Login window or selected account changed; stopping")
169
+ state.pop("login_identity", None)
170
+ for hwnd in self.windows():
171
+ if hwnd != self.hwnd and hwnd != self.main_hwnd:
172
+ kind = self.api.notice(str(hwnd))["result"].get("kind")
173
+ if kind:
174
+ state["notice"] = kind
175
+ self.failure_hwnd = hwnd
176
+ break
177
+ return state
178
+
179
+ def select_account(self, account, replace=False):
180
+ if self.already_logged_in():
181
+ if account and not self.gui.GetWindowText(self.main_hwnd).startswith(account + " - "):
182
+ raise RuntimeError("QMT 已登录的用户与配置的登录账号不一致,请重启该绑定 QMT")
183
+ return
184
+ if account and (replace or account != self.identity):
185
+ self.api.selectaccount(str(self.hwnd), account, replace)
186
+ self.identity = account
187
+ # Managed login clears remembered contents even for the same user.
188
+ self.api.fill(str(self.hwnd), "", False)
189
+
190
+ def submit(self):
191
+ state = self.state()
192
+ if not state["has_password"] or not state["login_enabled"]:
193
+ raise RuntimeError("Login is not ready")
194
+ self.api.submit(str(self.hwnd))
195
+
196
+ def fill(self, password):
197
+ self.state()
198
+ self.api.fill(str(self.hwnd), password, False)
199
+
200
+ def wait_outcome(self):
201
+ deadline = time.monotonic() + self.timeout
202
+ while time.monotonic() < deadline:
203
+ windows = self.windows()
204
+ for hwnd in windows:
205
+ caption = self.gui.GetWindowText(hwnd)
206
+ if self.identity and caption.startswith(self.identity + " - "):
207
+ if self.api.status(str(hwnd))["result"].get("is_main"):
208
+ return "logged_in"
209
+ for hwnd in windows:
210
+ if hwnd == self.hwnd:
211
+ try:
212
+ state = self.state()
213
+ except Exception:
214
+ if not self.gui.IsWindow(hwnd) or not self.gui.IsWindowVisible(hwnd):
215
+ continue
216
+ raise
217
+ kind = state.get("notice")
218
+ if not state["login_enabled"]:
219
+ continue
220
+ else:
221
+ kind = self.api.notice(str(hwnd))["result"].get("kind")
222
+ if kind:
223
+ self.failure_hwnd = hwnd
224
+ return kind
225
+ time.sleep(0.5)
226
+ # A slow connection is not evidence of a wrong password. Never resubmit.
227
+ return "timeout"
228
+
229
+ def acknowledge_failure(self):
230
+ if self.failure_hwnd and self.failure_hwnd != self.hwnd:
231
+ self.api.acknowledge(str(self.failure_hwnd))
232
+ self.failure_hwnd = None
233
+
234
+ def close(self):
235
+ import win32api
236
+ import win32event
237
+ try:
238
+ if self.session:
239
+ self.session.detach()
240
+ self.session = None
241
+ finally:
242
+ if self.mutex:
243
+ win32event.ReleaseMutex(self.mutex)
244
+ win32api.CloseHandle(self.mutex)
245
+ self.mutex = None
246
+
247
+
248
+ def main():
249
+ if hasattr(sys.stdout, "reconfigure"):
250
+ sys.stdout.reconfigure(encoding="utf-8")
251
+ parser = argparse.ArgumentParser(description=__doc__)
252
+ parser.add_argument("--binding", required=True)
253
+ parser.add_argument("--config", type=Path, default=Path(__file__).resolve().parents[1] / "runtime/config/cfquant_web_config.json")
254
+ parser.add_argument("--prompt", action="store_true", help="Read a needed password without echo; otherwise read one line of stdin")
255
+ parser.add_argument("--timeout", type=float, default=40)
256
+ parser.add_argument("--report", type=Path)
257
+ args = parser.parse_args()
258
+ if not 1 <= args.timeout <= 120:
259
+ parser.error("timeout must be between 1 and 120 seconds")
260
+ row, directory = select_binding(args.config, args.binding)
261
+ backend = QtLoginBackend(directory, args.timeout)
262
+ try:
263
+ backend.select_account((row.get("qmt_auto_login") or {}).get("login_account", ""), replace=True)
264
+ def read_password():
265
+ return getpass.getpass("QMT password (hidden): ") if args.prompt else sys.stdin.readline().rstrip("\r\n")
266
+ result = auto_login(backend, read_password)
267
+ result.update(binding=args.binding, process_id=backend.pid,
268
+ minimized_before=backend.minimized_before,
269
+ foreground_before=backend.foreground_before,
270
+ foreground_after=backend.gui.GetForegroundWindow(),
271
+ script_errors=backend.messages,
272
+ input_method="Qt native methods; no keyboard/mouse input")
273
+ output = json.dumps(result, ensure_ascii=False, indent=2)
274
+ if args.report:
275
+ args.report.write_text(output, encoding="utf-8")
276
+ print(output)
277
+ return 0 if result["outcome"] in ("logged_in", "already_logged_in") else 1
278
+ finally:
279
+ backend.close()
280
+
281
+
282
+ def login_running_qmt(directory, login_account="", password_provider=lambda: "", expected_pid=None,
283
+ startup_timeout=60, timeout=40):
284
+ """Wait for a freshly launched QMT, then execute one bounded login flow."""
285
+ deadline = time.monotonic() + startup_timeout
286
+ while True:
287
+ try:
288
+ backend = QtLoginBackend(Path(directory), timeout, expected_pid=expected_pid)
289
+ break
290
+ except QmtNotReady:
291
+ if time.monotonic() >= deadline:
292
+ return {"outcome": "startup_timeout", "login_succeeded": False, "attempts": []}
293
+ time.sleep(0.5)
294
+ try:
295
+ if not backend.already_logged_in() and backend.state().get("busy"):
296
+ outcome = backend.wait_outcome()
297
+ if outcome == "logged_in":
298
+ # Reopen only the automation session to verify the resulting user.
299
+ backend.close()
300
+ backend = QtLoginBackend(Path(directory), timeout, expected_pid=expected_pid)
301
+ elif outcome != "credential_error":
302
+ return {"outcome": outcome, "login_succeeded": False, "attempts": []}
303
+ backend.select_account(login_account, replace=True)
304
+ return auto_login(backend, password_provider)
305
+ finally:
306
+ backend.close()
307
+
308
+
309
+ if __name__ == "__main__":
310
+ sys.exit(main())
@@ -0,0 +1,72 @@
1
+ """Local QMT credentials protected for the current Windows user with DPAPI."""
2
+ import base64
3
+ import hashlib
4
+ import json
5
+ import os
6
+ from pathlib import Path
7
+ import threading
8
+
9
+ _LOCK = threading.RLock()
10
+
11
+
12
+ def directory_key(directory):
13
+ root = Path(directory).resolve()
14
+ if root.name.lower() in ("bin.x64", "python"):
15
+ root = root.parent
16
+ return hashlib.sha256(os.path.normcase(str(root)).encode("utf-8")).hexdigest()
17
+
18
+
19
+ def _protect(value, entropy):
20
+ import win32crypt
21
+ return win32crypt.CryptProtectData(value, "CFQuant QMT login", entropy, None, None, 1)
22
+
23
+
24
+ def _unprotect(value, entropy):
25
+ import win32crypt
26
+ return win32crypt.CryptUnprotectData(value, entropy, None, None, 1)[1]
27
+
28
+
29
+ class QmtCredentialStore:
30
+ def __init__(self, config_path):
31
+ self.path = Path(config_path).with_name("qmt_login_credentials.json")
32
+
33
+ def _read(self):
34
+ if not self.path.exists():
35
+ return {}
36
+ return json.loads(self.path.read_text(encoding="utf-8"))
37
+
38
+ def has(self, directory, login_account):
39
+ if not directory:
40
+ return False
41
+ with _LOCK:
42
+ entry = self._read().get(directory_key(directory), {})
43
+ return bool(entry.get("secret") and entry.get("login_account") == login_account)
44
+
45
+ def save(self, directory, login_account, password):
46
+ if not directory or not login_account:
47
+ raise ValueError("保存 QMT 密码时必须填写 QMT 目录和登录账号")
48
+ if not isinstance(password, str) or not password or len(password) > 1024:
49
+ raise ValueError("QMT 密码不能为空或超过 1024 字符")
50
+ key = directory_key(directory)
51
+ try:
52
+ encrypted = _protect(password.encode("utf-8"), key.encode("ascii"))
53
+ except Exception:
54
+ raise RuntimeError("无法使用当前 Windows 用户加密 QMT 密码,请检查 pywin32 安装") from None
55
+ with _LOCK:
56
+ data = self._read()
57
+ data[key] = {"login_account": login_account, "secret": base64.b64encode(encrypted).decode("ascii")}
58
+ self.path.parent.mkdir(parents=True, exist_ok=True)
59
+ temp = self.path.with_suffix(".%s.tmp" % os.getpid())
60
+ temp.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
61
+ os.replace(str(temp), str(self.path))
62
+
63
+ def password(self, directory, login_account):
64
+ key = directory_key(directory)
65
+ with _LOCK:
66
+ entry = self._read().get(key, {})
67
+ if not entry or entry.get("login_account") != login_account:
68
+ return ""
69
+ try:
70
+ return _unprotect(base64.b64decode(entry["secret"]), key.encode("ascii")).decode("utf-8")
71
+ except Exception:
72
+ raise RuntimeError("无法解密 QMT 密码,请使用保存密码时的 Windows 用户运行,或重新保存密码") from None
@@ -932,7 +932,7 @@ def test_save_account_runtime_config_parses_string_disabled(monkeypatch, tmp_pat
932
932
  monkeypatch.setattr(web, "CALLBACKS", SimpleNamespace(refresh_channels=lambda channels: None))
933
933
  monkeypatch.setattr(web, "callback_channels", lambda: [])
934
934
 
935
- result = web.save_account_runtime_config({
935
+ result = web.save_account_runtime_config({"qmt_auto_login": {"enabled": False},
936
936
  "account_id": "77557115",
937
937
  "account_type": "stock",
938
938
  "enabled": "false",
@@ -44,6 +44,7 @@ def assert_lite_package(manager, root):
44
44
  def web_deployment(deployment, tmp_path, monkeypatch):
45
45
  import cfquant_web_server as web
46
46
  manager, row, infos, running, root = deployment
47
+ row['qmt_auto_login'] = {'enabled': False}
47
48
  config = web.WebRuntimeConfig(str(tmp_path / "web.json"), str(tmp_path / "web.db"))
48
49
  config._data["account_configs"] = {row["account_key"]: row}
49
50
  monkeypatch.setattr(web, "WEB_CONFIG", config)