cfquant 0.2.67__tar.gz → 0.2.70__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 (310) hide show
  1. {cfquant-0.2.67 → cfquant-0.2.70}/PKG-INFO +1 -1
  2. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/__init__.py +2 -1
  3. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/client.py +2 -0
  4. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/monitor_notifications.py +3 -30
  5. cfquant-0.2.70/cfquant/notify.py +52 -0
  6. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_bridge.py +105 -30
  7. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_python_runtime.py +39 -10
  8. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_strategy_deploy.py +11 -3
  9. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_binding_python_runtime_browser.py +82 -2
  10. cfquant-0.2.70/cfquant/tests/test_binding_webhook_browser.py +106 -0
  11. cfquant-0.2.70/cfquant/tests/test_history_download_memory.py +68 -0
  12. cfquant-0.2.70/cfquant/tests/test_notify.py +214 -0
  13. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_auto_login_integration.py +32 -0
  14. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_python_runtime.py +180 -0
  15. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_strategy_deploy.py +19 -0
  16. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_trading_monitor.py +57 -0
  17. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_trading_monitor_browser.py +73 -0
  18. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/trading_monitor.py +16 -4
  19. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tx_trade_bridge.py +109 -30
  20. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/version.py +2 -2
  21. cfquant-0.2.70/cfquant/webhook.py +151 -0
  22. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/xtdata.py +2 -0
  23. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant.egg-info/PKG-INFO +1 -1
  24. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant.egg-info/SOURCES.txt +6 -0
  25. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant_web_server.py +262 -46
  26. {cfquant-0.2.67 → cfquant-0.2.70}/pyproject.toml +1 -1
  27. {cfquant-0.2.67 → cfquant-0.2.70}/qmt_scripts/CFQUANT_LITE.py +108 -31
  28. {cfquant-0.2.67 → cfquant-0.2.70}/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 +108 -31
  29. {cfquant-0.2.67 → cfquant-0.2.70}/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 +108 -31
  30. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/api-tester.js +1 -0
  31. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/app.js +212 -53
  32. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/index.html +44 -5
  33. cfquant-0.2.70/web_dashboard/notify-api-data.js +38 -0
  34. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/python-api.js +4 -4
  35. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/styles.css +8 -0
  36. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/trading-monitor.js +106 -16
  37. {cfquant-0.2.67 → cfquant-0.2.70}/LICENSE +0 -0
  38. {cfquant-0.2.67 → cfquant-0.2.70}/LTtx/__init__.py +0 -0
  39. {cfquant-0.2.67 → cfquant-0.2.70}/LTtx/tx/Config.txt +0 -0
  40. {cfquant-0.2.67 → cfquant-0.2.70}/LTtx/tx/LTtx_server.py +0 -0
  41. {cfquant-0.2.67 → cfquant-0.2.70}/LTtx/tx/__init__.py +0 -0
  42. {cfquant-0.2.67 → cfquant-0.2.70}/LTtx/tx/tx.py +0 -0
  43. {cfquant-0.2.67 → cfquant-0.2.70}/MANIFEST.in +0 -0
  44. {cfquant-0.2.67 → cfquant-0.2.70}/README.md +0 -0
  45. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/_editable_install.py +0 -0
  46. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/account_routing.py +0 -0
  47. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/algo.py +0 -0
  48. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/algo_broker.py +0 -0
  49. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/batch_orders.py +0 -0
  50. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/build_info.py +0 -0
  51. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/callback_dispatcher.py +0 -0
  52. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/cftrader.py +0 -0
  53. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/channels.py +0 -0
  54. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/cli.py +0 -0
  55. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/conditional_orders.py +0 -0
  56. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/config.py +0 -0
  57. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/diagnostic_context.py +0 -0
  58. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/diagnostics.py +0 -0
  59. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/flow_control.py +0 -0
  60. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/hub.py +0 -0
  61. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/level2.py +0 -0
  62. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/log_management.py +0 -0
  63. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/logging_i18n.py +0 -0
  64. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/management.py +0 -0
  65. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/monitor_performance.py +0 -0
  66. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/normal_bridge.py +0 -0
  67. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/order_identity.py +0 -0
  68. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/order_meta.py +0 -0
  69. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/pipe_bridge.py +0 -0
  70. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/pipe_client.py +0 -0
  71. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/pipe_hub.py +0 -0
  72. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/pipe_transport.py +0 -0
  73. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/planning_broker.py +0 -0
  74. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/position_sizing.py +0 -0
  75. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/protocol.py +0 -0
  76. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_binding_defaults.py +0 -0
  77. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_binding_settings.py +0 -0
  78. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_component_agent.js +0 -0
  79. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_component_login.py +0 -0
  80. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_credentials.py +0 -0
  81. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_dependencies.py +0 -0
  82. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_inner.py +0 -0
  83. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_inner_catalog.py +0 -0
  84. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_inner_runtime.py +0 -0
  85. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_mac_agent.js +0 -0
  86. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_native_files.py +0 -0
  87. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_native_worker.py +0 -0
  88. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_pipe_runtime.py +0 -0
  89. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_strategy_agent.js +0 -0
  90. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_strategy_package.py +0 -0
  91. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_strategy_recovery.py +0 -0
  92. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_strategy_runtime.py +0 -0
  93. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_update.py +0 -0
  94. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_vm_agent.js +0 -0
  95. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_vm_discovery.py +0 -0
  96. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_vm_runtime.py +0 -0
  97. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_vm_settings.py +0 -0
  98. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_vm_worker.py +0 -0
  99. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/quote_history.py +0 -0
  100. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/quote_leases.py +0 -0
  101. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/quote_service.py +0 -0
  102. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/remote_adapter.py +0 -0
  103. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/remote_management.py +0 -0
  104. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/remote_supervisor.py +0 -0
  105. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/rpc_timing.py +0 -0
  106. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/runtime_report.py +0 -0
  107. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/shared_quotes.py +0 -0
  108. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/smart_orders.py +0 -0
  109. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/stock_connect.py +0 -0
  110. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/subscription_bridge.py +0 -0
  111. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/subscription_client.py +0 -0
  112. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/subscription_manager.py +0 -0
  113. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/subscription_store.py +0 -0
  114. {cfquant-0.2.67 → cfquant-0.2.70}/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
  115. {cfquant-0.2.67 → cfquant-0.2.70}/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
  116. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/12_/350/256/242/345/215/225/345/244/207/346/263/250/346/265/213/350/257/225.py" +0 -0
  117. {cfquant-0.2.67 → cfquant-0.2.70}/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
  118. {cfquant-0.2.67 → cfquant-0.2.70}/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
  119. {cfquant-0.2.67 → cfquant-0.2.70}/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
  120. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/16_/345/220/257/345/212/250/345/256/211/350/243/205/346/265/213/350/257/225.py" +0 -0
  121. {cfquant-0.2.67 → cfquant-0.2.70}/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
  122. {cfquant-0.2.67 → cfquant-0.2.70}/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
  123. {cfquant-0.2.67 → cfquant-0.2.70}/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
  124. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/1_/350/241/214/346/203/205/346/216/245/346/224/266/346/265/213/350/257/225.py" +0 -0
  125. {cfquant-0.2.67 → cfquant-0.2.70}/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
  126. {cfquant-0.2.67 → cfquant-0.2.70}/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
  127. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/22_/350/277/236/346/216/245/350/257/212/346/226/255/346/265/213/350/257/225.py" +0 -0
  128. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/23_/347/244/272/344/276/213/351/205/215/347/275/256/346/265/213/350/257/225.py" +0 -0
  129. {cfquant-0.2.67 → cfquant-0.2.70}/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
  130. {cfquant-0.2.67 → cfquant-0.2.70}/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
  131. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/26_/345/205/250/346/216/250/350/256/242/351/230/205/346/265/213/350/257/225.py" +0 -0
  132. {cfquant-0.2.67 → cfquant-0.2.70}/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
  133. {cfquant-0.2.67 → cfquant-0.2.70}/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
  134. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/29_Level2/350/241/214/346/203/205/346/265/213/350/257/225.py" +0 -0
  135. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/2_/346/225/260/346/215/256/350/216/267/345/217/226/346/265/213/350/257/225.py" +0 -0
  136. {cfquant-0.2.67 → cfquant-0.2.70}/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
  137. {cfquant-0.2.67 → cfquant-0.2.70}/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
  138. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/32_/345/274/202/346/255/245/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  139. {cfquant-0.2.67 → cfquant-0.2.70}/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
  140. {cfquant-0.2.67 → cfquant-0.2.70}/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
  141. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/3_/346/225/260/346/215/256/344/270/213/350/275/275/346/265/213/350/257/225.py" +0 -0
  142. {cfquant-0.2.67 → cfquant-0.2.70}/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
  143. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/5_/347/234/237/345/256/236/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  144. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/6_/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
  145. {cfquant-0.2.67 → cfquant-0.2.70}/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
  146. {cfquant-0.2.67 → cfquant-0.2.70}/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
  147. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/9_/345/217/257/346/222/244/350/256/242/345/215/225/346/265/213/350/257/225.py" +0 -0
  148. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/_helpers.py +0 -0
  149. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/qmt_vm_native_fixture.js +0 -0
  150. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/readme.md +0 -0
  151. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_account_query_timing.py +0 -0
  152. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_account_subscription_probe.py +0 -0
  153. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_activity_log_browser.py +0 -0
  154. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_advanced_parity.py +0 -0
  155. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_algo.py +0 -0
  156. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_algo_broker.py +0 -0
  157. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_api_tester_browser.py +0 -0
  158. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_api_tester_web.py +0 -0
  159. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_auto_login_app.py +0 -0
  160. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
  161. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_binding_live_refresh_browser.py +0 -0
  162. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_binding_runtime_browser.py +0 -0
  163. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_binding_vm_browser.py +0 -0
  164. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_callback_recovery.py +0 -0
  165. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_callback_requests.py +0 -0
  166. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_cftrader.py +0 -0
  167. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_cftrader_tutorial.py +0 -0
  168. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_cftrader_web.py +0 -0
  169. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_credit_compat.py +0 -0
  170. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_dataframe_encoding.py +0 -0
  171. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_diagnostic_context.py +0 -0
  172. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_json_serialization.py +0 -0
  173. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_latency_flow_control.py +0 -0
  174. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_lite_account_routing.py +0 -0
  175. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_lite_deployment_paths.py +0 -0
  176. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_lite_parity.py +0 -0
  177. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_lite_qmt_dispatch.py +0 -0
  178. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_local_trading_calendar.py +0 -0
  179. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_log_management.py +0 -0
  180. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_management.py +0 -0
  181. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_management_batch.py +0 -0
  182. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_market_data_fallback.py +0 -0
  183. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_market_timing.py +0 -0
  184. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_monitor_notifications.py +0 -0
  185. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_monitor_performance.py +0 -0
  186. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_navigation_browser.py +0 -0
  187. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_order_identity.py +0 -0
  188. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_order_time_callback.py +0 -0
  189. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_pipe_shutdown.py +0 -0
  190. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_project_updater.py +0 -0
  191. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_python_api_reference.py +0 -0
  192. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_auto_login_browser.py +0 -0
  193. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_binding_defaults.py +0 -0
  194. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_binding_settings.py +0 -0
  195. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_component_login.py +0 -0
  196. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_dependencies.py +0 -0
  197. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_fileless.py +0 -0
  198. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_inner.py +0 -0
  199. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_inner_reference.py +0 -0
  200. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_inner_web.py +0 -0
  201. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_native_files.py +0 -0
  202. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_native_reuse.py +0 -0
  203. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_native_worker.py +0 -0
  204. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_package_import.py +0 -0
  205. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_strategy_browser.py +0 -0
  206. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_strategy_capabilities.py +0 -0
  207. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_strategy_recovery.py +0 -0
  208. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_strategy_web.py +0 -0
  209. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_update.py +0 -0
  210. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_vm_discovery.py +0 -0
  211. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_vm_native.py +0 -0
  212. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_vm_runtime.py +0 -0
  213. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_vm_settings.py +0 -0
  214. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_quote_latency.py +0 -0
  215. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_quote_latency_encoding.py +0 -0
  216. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_quote_service.py +0 -0
  217. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_remote_management.py +0 -0
  218. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_remote_management_browser.py +0 -0
  219. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_remote_version_check.py +0 -0
  220. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
  221. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_shared_quotes.py +0 -0
  222. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_shared_quotes_generated.py +0 -0
  223. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_smart_orders.py +0 -0
  224. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_smart_orders_api.py +0 -0
  225. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_smart_orders_docs.py +0 -0
  226. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_split_order_identity.py +0 -0
  227. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_startup_recovery.py +0 -0
  228. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_stock_connect.py +0 -0
  229. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_stock_connect_browser.py +0 -0
  230. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_strategy_recovery_api.py +0 -0
  231. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_subscription_congestion.py +0 -0
  232. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_subscription_persistence.py +0 -0
  233. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_table_conversion.py +0 -0
  234. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_trade_strategy_callback.py +0 -0
  235. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_trader_subscription_recovery.py +0 -0
  236. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_trading_dates.py +0 -0
  237. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_tutorial_reader.py +0 -0
  238. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_tx_auto_import.py +0 -0
  239. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_tx_shutdown.py +0 -0
  240. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_version_check.py +0 -0
  241. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_version_check_browser.py +0 -0
  242. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tools/THIRD_PARTY.md +0 -0
  243. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tools/upx/COPYING +0 -0
  244. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tools/upx/LICENSE +0 -0
  245. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tools/upx/NEWS +0 -0
  246. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tools/upx/README +0 -0
  247. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tools/upx/THANKS.txt +0 -0
  248. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tools/upx/upx-doc.html +0 -0
  249. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tools/upx/upx-doc.txt +0 -0
  250. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tools/upx/upx.1 +0 -0
  251. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tools/upx/upx.exe +0 -0
  252. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/trading_calendar.py +0 -0
  253. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tx.py +0 -0
  254. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/xtconstant.py +0 -0
  255. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/xttrader.py +0 -0
  256. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/xttype.py +0 -0
  257. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant.egg-info/dependency_links.txt +0 -0
  258. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant.egg-info/entry_points.txt +0 -0
  259. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant.egg-info/requires.txt +0 -0
  260. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant.egg-info/top_level.txt +0 -0
  261. {cfquant-0.2.67 → cfquant-0.2.70}/cfquant_pipe_hub.py +0 -0
  262. {cfquant-0.2.67 → cfquant-0.2.70}/qmt_scripts/CFQUANT.py +0 -0
  263. {cfquant-0.2.67 → cfquant-0.2.70}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +0 -0
  264. {cfquant-0.2.67 → cfquant-0.2.70}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
  265. {cfquant-0.2.67 → cfquant-0.2.70}/qmt_scripts/__init__.py +0 -0
  266. {cfquant-0.2.67 → cfquant-0.2.70}/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
  267. {cfquant-0.2.67 → cfquant-0.2.70}/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
  268. {cfquant-0.2.67 → cfquant-0.2.70}/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
  269. {cfquant-0.2.67 → cfquant-0.2.70}/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
  270. {cfquant-0.2.67 → cfquant-0.2.70}/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
  271. {cfquant-0.2.67 → cfquant-0.2.70}/requirements.txt +0 -0
  272. {cfquant-0.2.67 → cfquant-0.2.70}/setup.cfg +0 -0
  273. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/__init__.py +0 -0
  274. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/activity-log.js +0 -0
  275. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/avatars/amber-pulse.svg +0 -0
  276. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/avatars/copper-grid.svg +0 -0
  277. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/avatars/market-blue.svg +0 -0
  278. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/avatars/rose-circuit.svg +0 -0
  279. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/avatars/signal-green.svg +0 -0
  280. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/avatars/slate-wave.svg +0 -0
  281. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/avatars/teal-orbit.svg +0 -0
  282. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/avatars/violet-node.svg +0 -0
  283. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/cfquant-architecture.html +0 -0
  284. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/cftrader-api-data.js +0 -0
  285. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/docs.js +0 -0
  286. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/favicon.svg +0 -0
  287. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
  288. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
  289. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
  290. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
  291. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
  292. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
  293. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
  294. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
  295. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
  296. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
  297. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
  298. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
  299. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
  300. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
  301. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
  302. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
  303. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
  304. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
  305. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/navigation.js +0 -0
  306. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/python-api-data.js +0 -0
  307. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/qmt-inner-api-data.js +0 -0
  308. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/remote-management.js +0 -0
  309. {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/smart-order-api-data.js +0 -0
  310. {cfquant-0.2.67 → cfquant-0.2.70}/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.67
3
+ Version: 0.2.70
4
4
  Summary: QMT local bridge and xtquant-compatible Python client.
5
5
  License: MIT
6
6
  Requires-Python: >=3.8
@@ -12,7 +12,7 @@ cfquant: 面向 QMT 的 xtquant 兼容层。
12
12
  import sys as _sys
13
13
 
14
14
  from . import xtconstant, xtdata, xttrader, xttype
15
- from . import cftrader
15
+ from . import cftrader, notify
16
16
  from .qmt_inner import QmtInnerApi
17
17
  from .client import CfquantError, CfquantTimeout, configure, get_client
18
18
  from .version import __version__
@@ -28,6 +28,7 @@ __all__ = [
28
28
  "xtdata",
29
29
  "xttrader",
30
30
  "cftrader",
31
+ "notify",
31
32
  "QmtInnerApi",
32
33
  "xttype",
33
34
  "configure",
@@ -472,6 +472,8 @@ def configure(**kwargs):
472
472
  from .config import configure as configure_config
473
473
 
474
474
  configure_config(**kwargs)
475
+ from . import notify
476
+ notify.close()
475
477
  global _default_client
476
478
  with _client_lock:
477
479
  if _default_client is not None:
@@ -6,11 +6,9 @@ never perform SMTP or HTTP work on a QMT callback/status thread.
6
6
  """
7
7
 
8
8
  import copy
9
- import base64
10
9
  from datetime import datetime, timedelta, timezone
11
10
  from email.message import EmailMessage
12
11
  import hashlib
13
- import hmac
14
12
  import json
15
13
  import os
16
14
  from pathlib import Path
@@ -20,9 +18,7 @@ import ssl
20
18
  import tempfile
21
19
  import threading
22
20
  import time
23
- import urllib.error
24
- import urllib.parse
25
- import urllib.request
21
+ from .webhook import send_webhook
26
22
 
27
23
 
28
24
  SHANGHAI = timezone(timedelta(hours=8))
@@ -566,28 +562,5 @@ class MonitorNotificationService:
566
562
  server.send_message(msg)
567
563
 
568
564
  def _send_webhook(self, config, event):
569
- url = str(config.get("url") or "").strip()
570
- if not url:
571
- raise ValueError("webhook url is required")
572
- kind = str(config.get("type") or "generic").lower()
573
- if kind == "dingtalk" and config.get("secret"):
574
- timestamp = str(int(time.time() * 1000))
575
- secret = str(config.get("secret"))
576
- raw = "%s\n%s" % (timestamp, secret)
577
- sign = base64.b64encode(hmac.new(secret.encode("utf-8"), raw.encode("utf-8"), hashlib.sha256).digest()).decode("ascii")
578
- separator = "&" if "?" in url else "?"
579
- url = "%s%stimestamp=%s&sign=%s" % (url, separator, urllib.parse.quote(timestamp), urllib.parse.quote(sign))
580
- text = self._format_text(event)
581
- if kind == "feishu":
582
- body = {"msg_type": "text", "content": {"text": text}}
583
- elif kind in ("dingtalk", "wecom"):
584
- body = {"msgtype": "text", "text": {"content": text}}
585
- else:
586
- body = {"event_type": event.get("event_type"), "title": event.get("title"),
587
- "message": event.get("message"), "payload": event.get("payload"),
588
- "occurred_at": event.get("occurred_at")}
589
- request = urllib.request.Request(url, data=json.dumps(body, ensure_ascii=False, default=str).encode("utf-8"),
590
- headers={"Content-Type": "application/json; charset=utf-8", "User-Agent": "cfquant-monitor"}, method="POST")
591
- with urllib.request.urlopen(request, timeout=8) as response:
592
- if int(getattr(response, "status", 200)) >= 400:
593
- raise urllib.error.HTTPError(url, response.status, "webhook returned %s" % response.status, response.headers, None)
565
+ payload = {key: event.get(key) for key in ("event_type", "title", "message", "payload", "occurred_at")}
566
+ return send_webhook(config, self._format_text(event), payload=payload)
@@ -0,0 +1,52 @@
1
+ """User-authored messages: explicit Webhook first, otherwise account binding."""
2
+
3
+ import atexit
4
+ import threading
5
+
6
+ from .webhook import WebhookError, normalize_webhook, send_webhook, validate_message
7
+
8
+ _client = None
9
+ _client_lock = threading.Lock()
10
+
11
+
12
+ def close():
13
+ """Release the notification RPC connection; the next call reconnects."""
14
+ global _client
15
+ with _client_lock:
16
+ client, _client = _client, None
17
+ if client is not None:
18
+ client.close()
19
+
20
+
21
+ atexit.register(close)
22
+
23
+
24
+ def send(message, account_id=None, *, title="", webhook_url=None, provider=None,
25
+ secret=None, account_type=None, account_key=None, bridge_id=None, timeout=8):
26
+ """Send text synchronously; returns sent/provider/source, or raises on failure.
27
+
28
+ webhook_url and provider must be supplied together and override the binding
29
+ completely (including its secret). Account-based sends require cfquant Web
30
+ and LTtx, but do not require QMT to be online. No automatic retry is performed.
31
+ """
32
+ timeout = validate_message(message, title, timeout)
33
+ if webhook_url is not None or provider is not None or secret is not None:
34
+ if not webhook_url or not provider:
35
+ raise ValueError("调用时覆盖推送配置必须同时提供 webhook_url 和 provider")
36
+ config = normalize_webhook({"enabled": True, "url": webhook_url, "provider": provider, "secret": secret})
37
+ return dict(send_webhook(config, message, title=title, timeout=timeout), source="explicit")
38
+ if not str(account_id or "").strip() and not str(account_key or "").strip():
39
+ raise ValueError("请指定资金账号 account_id 或 account_key,或提供 webhook_url 和 provider")
40
+ params = {"message": message, "title": title, "timeout": timeout}
41
+ params.update({key: value for key, value in dict(account_id=account_id, account_type=account_type,
42
+ account_key=account_key, bridge_id=bridge_id).items() if value is not None})
43
+ global _client
44
+ with _client_lock:
45
+ if _client is None:
46
+ from .client import create_rpc_client
47
+ _client = create_rpc_client(transport="web_lttx")
48
+ client = _client
49
+ return client.request("cfquant.notify.send", params, timeout=timeout + 5)
50
+
51
+
52
+ __all__ = ["send", "close", "WebhookError"]
@@ -1,6 +1,7 @@
1
1
  from .diagnostic_context import versioned_message
2
2
  # -*- coding: utf-8 -*-
3
3
  import os
4
+ import gc
4
5
  import queue
5
6
  import sys
6
7
  import threading
@@ -516,47 +517,91 @@ class CfquantQmtBridge(object):
516
517
  })
517
518
  raise NotImplementedError("当前QMT环境未提供download_history_data2")
518
519
 
519
- def callback(data):
520
+ stock_list = params.get("stock_list", params.get("code_list", []))
521
+ if isinstance(stock_list, str):
522
+ stock_list = [item.strip() for item in stock_list.replace(",", ",").split(",") if item.strip()]
523
+ else:
524
+ stock_list = list(stock_list or [])
525
+ batches = self._history_download_batches(stock_list, params)
526
+ total = len(stock_list)
527
+ completed = 0
528
+
529
+ def callback(data, batch_offset=0, batch_index=0, batch_total=1):
520
530
  if callback_event and client_id:
531
+ payload = dict(data) if isinstance(data, dict) else {"message": str(data)}
532
+ batch_finished = payload.get("finished")
533
+ try:
534
+ batch_finished = max(0, min(int(batch_finished), len(batches[batch_index])))
535
+ except (TypeError, ValueError, IndexError):
536
+ batch_finished = 0
537
+ payload.update({
538
+ "finished": min(total, batch_offset + batch_finished),
539
+ "total": total,
540
+ "batch_index": batch_index + 1,
541
+ "batch_total": batch_total,
542
+ "batch_finished": batch_finished,
543
+ "batch_size": len(batches[batch_index]) if batch_index < len(batches) else 0,
544
+ })
521
545
  self._send_event(
522
546
  client_id,
523
547
  callback_event,
524
- data,
548
+ payload,
525
549
  meta=self._download_event_meta(params, "history", "progress"),
526
550
  )
527
551
 
528
- callback_func = callback if callback_event else None
529
- variants = []
530
- incrementally = params.get("incrementally")
531
- if incrementally is not None:
532
- variants.append((
533
- (
534
- params.get("stock_list", params.get("code_list", [])),
535
- params.get("period", "1d"),
536
- params.get("start_time", ""),
537
- params.get("end_time", ""),
538
- callback_func,
539
- incrementally,
540
- ),
541
- {},
542
- ))
543
- variants.append((
544
- (
545
- params.get("stock_list", params.get("code_list", [])),
546
- params.get("period", "1d"),
547
- params.get("start_time", ""),
548
- params.get("end_time", ""),
549
- callback_func,
550
- ),
551
- {},
552
- ))
553
552
  if emit_lifecycle:
554
553
  self._send_download_event(client_id, params, "history", "submitted", {
555
554
  "stage": "submitted",
556
555
  "message": "history download request submitted",
556
+ "total": total,
557
+ "batch_total": len(batches),
558
+ "batch_size": self._history_download_batch_size(params),
557
559
  })
558
560
  try:
559
- result = self._call_variants(func, variants)
561
+ incrementally = params.get("incrementally")
562
+ batch_total = len(batches)
563
+ for batch_index, batch in enumerate(batches):
564
+ batch_offset = completed
565
+ callback_func = (
566
+ (lambda data, offset=batch_offset, index=batch_index:
567
+ callback(data, offset, index, batch_total))
568
+ if callback_event else None
569
+ )
570
+ variants = []
571
+ if incrementally is not None:
572
+ variants.append((
573
+ (
574
+ batch,
575
+ params.get("period", "1d"),
576
+ params.get("start_time", ""),
577
+ params.get("end_time", ""),
578
+ callback_func,
579
+ incrementally,
580
+ ),
581
+ {},
582
+ ))
583
+ variants.append((
584
+ (
585
+ batch,
586
+ params.get("period", "1d"),
587
+ params.get("start_time", ""),
588
+ params.get("end_time", ""),
589
+ callback_func,
590
+ ),
591
+ {},
592
+ ))
593
+ self._call_variants(func, variants)
594
+ completed += len(batch)
595
+ if emit_lifecycle:
596
+ self._send_download_event(client_id, params, "history", "batch_done", {
597
+ "stage": "batch_done",
598
+ "finished": completed,
599
+ "total": total,
600
+ "batch_index": batch_index + 1,
601
+ "batch_total": batch_total,
602
+ "batch_size": len(batch),
603
+ })
604
+ gc.collect()
560
605
  except Exception as e:
561
606
  if emit_lifecycle:
562
607
  self._send_download_event(client_id, params, "history", "error", {
@@ -567,9 +612,39 @@ class CfquantQmtBridge(object):
567
612
  if emit_lifecycle:
568
613
  self._send_download_event(client_id, params, "history", "request_done", {
569
614
  "stage": "request_done",
570
- "result": result,
615
+ "result": None,
616
+ "finished": completed,
617
+ "total": total,
571
618
  })
572
- return result
619
+ return None
620
+
621
+ @staticmethod
622
+ def _history_download_batch_size(params):
623
+ params = params or {}
624
+ value = params.get("download_batch_size", params.get("batch_size"))
625
+ if value is None:
626
+ value = os.environ.get("CFQUANT_HISTORY_DOWNLOAD_BATCH_SIZE")
627
+ if value is None or str(value).strip() == "":
628
+ period = str(params.get("period") or "").lower()
629
+ if period == "tick":
630
+ return 20
631
+ if period == "1m":
632
+ return 50
633
+ if period.endswith("m") or period.endswith("h"):
634
+ return 100
635
+ return 200
636
+ try:
637
+ value = int(value)
638
+ except (TypeError, ValueError):
639
+ return 50
640
+ return max(1, value)
641
+
642
+ @classmethod
643
+ def _history_download_batches(cls, stock_list, params):
644
+ if not stock_list:
645
+ return [stock_list]
646
+ size = cls._history_download_batch_size(params)
647
+ return [stock_list[index:index + size] for index in range(0, len(stock_list), size)]
573
648
 
574
649
  def _get_instrument_detail(self, params):
575
650
  func = getattr(self.context, "get_instrument_detail")
@@ -593,7 +593,8 @@ class QmtPythonRuntime:
593
593
  time.sleep(0.25)
594
594
  raise QmtPythonRuntimeError("QMT did not start after runtime switch")
595
595
 
596
- def validate_selection(self, runtime, *, account_id=None):
596
+ def validate_selection(self, runtime, *, account_id=None,
597
+ allow_missing_account_config=False):
597
598
  runtime = _canonical_runtime(runtime)
598
599
  effective_account = account_id if account_id is not None else self.account_id
599
600
  binding = self.binding_status(account_id=effective_account)
@@ -603,6 +604,8 @@ class QmtPythonRuntime:
603
604
  % (binding.get("qmt_version") or "unknown"))
604
605
  if effective_account:
605
606
  if not binding["config_exists"]:
607
+ if allow_missing_account_config and binding["state"] == "account_config_missing":
608
+ return binding
606
609
  raise QmtPythonRuntimeError("QMT account Config.xml not found: %s; log in to QMT first" % effective_account)
607
610
  text = Path(binding["config"]).read_text(encoding="utf-8")
608
611
  if not re.search(r'<python\b[^>]*\blibs_path="[^"]*"', text):
@@ -611,9 +614,19 @@ class QmtPythonRuntime:
611
614
 
612
615
  def download_and_switch(self, runtime="py312", *, restart=True, account_id=None,
613
616
  progress_callback=None, cancel_event=None,
614
- restart_callback=None, reuse_installed=False):
617
+ restart_callback=None, reuse_installed=False,
618
+ allow_missing_account_config=False):
615
619
  runtime = _canonical_runtime(runtime)
616
- binding = self.validate_selection(runtime, account_id=account_id)
620
+ binding = self.validate_selection(
621
+ runtime,
622
+ account_id=account_id,
623
+ allow_missing_account_config=allow_missing_account_config,
624
+ )
625
+ deferred_account_config = bool(
626
+ allow_missing_account_config
627
+ and (account_id or self.account_id)
628
+ and binding.get("state") == "account_config_missing"
629
+ )
617
630
  reuse = bool(reuse_installed and binding["runtimes"][runtime]["installed"])
618
631
  package, archive = None, None
619
632
  if reuse:
@@ -624,17 +637,30 @@ class QmtPythonRuntime:
624
637
  cancel_event=cancel_event)
625
638
  if cancel_event and cancel_event.is_set():
626
639
  raise QmtPythonRuntimeError("QMT Python runtime operation cancelled")
627
- _callback(progress_callback, QmtPythonProgress(runtime, "activating", message="正在停止 QMT 并配置执行器"))
628
- if restart and self._qmt_processes():
640
+ _callback(progress_callback, QmtPythonProgress(
641
+ runtime, "activating",
642
+ message=("正在停止 QMT 并安装运行库" if deferred_account_config
643
+ else "正在停止 QMT 并配置执行器"),
644
+ ))
645
+ was_running = bool(self._qmt_processes())
646
+ if restart and was_running:
629
647
  self._stop_for_switch()
630
- elif not restart and self._qmt_processes():
648
+ elif not restart and was_running:
631
649
  raise QmtPythonRuntimeError(
632
- "QMT is running; set restart=True before activating a Python runtime")
650
+ "QMT is running; set restart=True before installing or activating a Python runtime")
633
651
  try:
634
652
  installed = self.inspect(runtime, refresh_remote=False) if reuse else self.install(runtime, package=package, progress_callback=progress_callback)
635
- switched = self.switch_account(runtime, account_id=account_id) if (account_id or self.account_id) else None
653
+ switched = None if deferred_account_config else (
654
+ self.switch_account(runtime, account_id=account_id)
655
+ if (account_id or self.account_id) else None
656
+ )
636
657
  finally:
637
- if restart:
658
+ # A managed Web binding can supply a bootstrap callback that starts
659
+ # QMT, waits for the first login to create Config.xml, and then
660
+ # performs the final runtime switch/restart. Keep the standalone
661
+ # API's old safe behavior (leave a brand-new, unconfigured QMT
662
+ # stopped) when no callback was supplied.
663
+ if restart and (not deferred_account_config or was_running or restart_callback):
638
664
  _callback(progress_callback, QmtPythonProgress(runtime, "restarting", message="正在重新启动 QMT"))
639
665
  if restart_callback:
640
666
  restarted = restart_callback()
@@ -646,7 +672,10 @@ class QmtPythonRuntime:
646
672
  restarted = None
647
673
  return {"runtime": runtime, "package": package.as_dict() if package else None,
648
674
  "archive": str(archive) if archive else None, "reused_installed": reuse,
649
- "installed": installed, "switched": switched, "restarted": restarted}
675
+ "installed": installed, "switched": switched, "restarted": restarted,
676
+ "account_config_required": deferred_account_config,
677
+ "message": ("运行库已安装;请先在 QMT 登录此账号,再重新检测并应用执行器。"
678
+ if deferred_account_config else "")}
650
679
 
651
680
  def _stop_for_switch(self):
652
681
  for process in self._qmt_processes():
@@ -82,7 +82,7 @@ STATES = {
82
82
  "waiting_start": "QMT 已启动,等待托管策略启动及通道连接",
83
83
  "waiting_manual_start": "QMT 已启动,自启未开启,请在模型交易中运行托管策略",
84
84
  "running": "托管策略已运行",
85
- "waiting_account": "未找到此资金账号的 QMT 绑定键,请补充模型账号 Key",
85
+ "waiting_account": "等待 QMT 登录生成账号绑定键;首次登录后请正常退出 QMT,再重新启动以完成策略部署。仍无法识别时再补充模型账号 Key",
86
86
  "configured": "模型已配置,等待下次启动 QMT 生效",
87
87
  "disabled": "自动运行已关闭,托管策略已禁用",
88
88
  "error": "策略部署失败",
@@ -915,15 +915,16 @@ class QmtStrategyManager:
915
915
  self._save()
916
916
 
917
917
  def prepare_for_start(self, directory):
918
- """Finish this terminal's offline deployment before allowing launch."""
918
+ """Finish offline deployment, or allow login to supply a missing key."""
919
919
  root = qmt_root(directory)
920
+ waiting_account = []
920
921
  with self.lock:
921
922
  if self.load_error:
922
923
  raise ValueError(self.load_error)
923
924
  jobs = [job for job in self.jobs.values()
924
925
  if os.path.normcase(str(qmt_root(job["root"]))) == os.path.normcase(str(root))]
925
926
  if not jobs:
926
- return
927
+ return {"waiting_account": waiting_account}
927
928
  if self.process_checker(root):
928
929
  raise RuntimeError("QMT 尚未完全退出,无法完成启动前策略部署")
929
930
  try:
@@ -957,6 +958,12 @@ class QmtStrategyManager:
957
958
  job["deploy_retry_after"] = time.time() + min(10.0, 2.0 * count)
958
959
  self._state(job, "error", str(error))
959
960
  raise
961
+ if job.get("state") == "waiting_account":
962
+ # QMT creates its account authorization on first login.
963
+ # Do not fabricate a model key or make login depend on
964
+ # the model that can only be prepared after that login.
965
+ waiting_account.append(job["account_id"])
966
+ continue
960
967
  if (job.get("state") not in ("configured", "waiting_import", "disabled")
961
968
  or (job.get("enabled") and any(
962
969
  not role.get("model_prepared") or role.get("reimport_required")
@@ -965,6 +972,7 @@ class QmtStrategyManager:
965
972
  (job.get("message") or job.get("state", "")))
966
973
  finally:
967
974
  self._save()
975
+ return {"waiting_account": waiting_account}
968
976
 
969
977
  def _advance(self, job):
970
978
  root = qmt_root(job["root"])
@@ -81,6 +81,15 @@ def test_selected_executor_is_applied_through_existing_job_until_restart_finishe
81
81
  expect(button).to_be_disabled()
82
82
  assert not page.locator("#bindingPythonRuntimeProgressBar").evaluate("node => node.classList.contains('is-done')")
83
83
  expect(page.locator("#bindingPythonRuntimeProgressMeta")).to_contain_text("等待切换")
84
+ for phase in ("extract", "activating", "restarting"):
85
+ page.evaluate("""phase => bindingPythonRuntimeProgressEvent({
86
+ meta: {job_id: state.bindingPythonRuntimeJobId, stage: phase},
87
+ data: {phase, percent: 0, eta: null}
88
+ })""", phase)
89
+ expect(page.locator("#bindingPythonRuntimeProgressMeta")).not_to_contain_text("0%")
90
+ expect(page.locator("#bindingPythonRuntimeProgressMeta")).not_to_contain_text("剩余约 0")
91
+ assert page.locator("#bindingPythonRuntimeProgressBar").evaluate("node => node.getBoundingClientRect().width > 0")
92
+ assert page.evaluate("!!state.bindingPythonRuntimeJobId")
84
93
  status["binding"].update(selected_runtime="py312", configured_runtime="py312", state="ready")
85
94
  status["binding"]["runtimes"]["py312"]["installed"] = True
86
95
  page.evaluate("""bindingPythonRuntimeProgressEvent({
@@ -95,6 +104,45 @@ def test_selected_executor_is_applied_through_existing_job_until_restart_finishe
95
104
  assert not errors
96
105
 
97
106
 
107
+ def test_lost_job_record_releases_busy_state_without_claiming_success(page, frontend_url):
108
+ errors = open_runtime(page, frontend_url, runtime_status(config_exists=False))
109
+ page.route("**/api/qmt/python-runtime/download", lambda route: route.fulfill(json={"ok": True, "data": {"state": "queued"}}))
110
+ page.route("**/api/qmt/python-runtime/jobs?*", lambda route: route.fulfill(status=404, json={"ok": False, "error": "job not found"}))
111
+ page.locator("#bindingPythonRuntimeDownloadBtn").click()
112
+ page.wait_for_function("!state.bindingPythonRuntimeJobId && state.bindingPythonRuntimeProgress?.meta?.stage === 'error'")
113
+ expect(page.locator("#bindingPythonRuntimeProgressText")).to_contain_text("记录已失效")
114
+ expect(page.locator("#bindingPythonRuntimeDownloadBtn")).to_be_enabled()
115
+ assert not errors
116
+
117
+
118
+ def test_save_rechecks_completed_runtime_job_even_without_progress_notification(page, frontend_url):
119
+ errors = open_runtime(page, frontend_url, runtime_status(config_exists=False))
120
+ page.route("**/api/qmt/python-runtime/jobs?*", lambda route: route.fulfill(json={
121
+ "ok": True, "data": {"state": "done", "result": {"account_config_required": True}}}))
122
+ saved = []
123
+ def save(route):
124
+ body = route.request.post_data_json
125
+ saved.append(body)
126
+ row = dict(body, account_key="TEST_ONLY|STOCK|test", bridge_id="test")
127
+ route.fulfill(json={"ok": True, "data": {
128
+ "account": row, "account_configs": {row["account_key"]: row}, "account_pairs": {},
129
+ "bridges": {"test": {"name": "Test"}}, "qmt_strategy_deploy": {"enabled": False},
130
+ "qmt_auto_login": {"enabled": False}}})
131
+ page.route("**/api/account-config", save)
132
+ page.locator("#bindingQmtAutoLogin").uncheck()
133
+ page.evaluate("""() => {
134
+ fillQmtStrategySettings('binding', {enabled: false});
135
+ state.bindingPythonRuntimeJobId = 'missed-done';
136
+ state.bindingPythonRuntimeJobContext = bindingPythonRuntimeContextKey();
137
+ state.bindingPythonRuntimeJobRuntime = 'py36';
138
+ }""")
139
+ page.locator('#bindingForm [type="submit"]').click()
140
+ expect(page.locator("#bindingDialogOverlay")).not_to_be_visible()
141
+ assert len(saved) == 1
142
+ assert not page.evaluate("state.bindingPythonRuntimeJobId")
143
+ assert not errors
144
+
145
+
98
146
  def test_directory_change_invalidates_options_and_discards_late_response(page, frontend_url):
99
147
  errors = open_runtime(page, frontend_url, runtime_status())
100
148
  pending = []
@@ -119,6 +167,38 @@ def test_missing_account_config_shows_capabilities_but_requires_login(page, fron
119
167
  errors = open_runtime(page, frontend_url, runtime_status(config_exists=False))
120
168
  expect(page.locator("#bindingPythonRuntimeSelect option")).to_have_count(2)
121
169
  page.locator("#bindingPythonRuntimeSelect").select_option("py312")
122
- expect(page.locator("#bindingPythonRuntimeDownloadBtn")).to_be_disabled()
123
- expect(page.locator("#bindingPythonRuntimeChoiceHint")).to_contain_text("登录")
170
+ submitted = []
171
+ page.route("**/api/qmt/python-runtime/download", lambda route: (
172
+ submitted.append(route.request.post_data_json),
173
+ route.fulfill(json={"ok": True, "data": {"state": "queued"}})))
174
+ expect(page.locator("#bindingPythonRuntimeDownloadBtn")).to_be_enabled()
175
+ expect(page.locator("#bindingPythonRuntimeChoiceHint")).to_contain_text("先下载")
176
+ page.locator("#bindingPythonRuntimeDownloadBtn").click()
177
+ page.wait_for_function("state.bindingPythonRuntimeJobId")
178
+ assert submitted[0]["allow_missing_account_config"] is True
179
+ assert not errors
180
+
181
+
182
+ @pytest.mark.parametrize("socket_throws", [False, True], ids=["socket-disconnected", "socket-unavailable"])
183
+ def test_poll_completion_before_first_login_unlocks_binding(page, frontend_url, socket_throws):
184
+ status = runtime_status(config_exists=False)
185
+ errors = open_runtime(page, frontend_url, status)
186
+ submitted = []
187
+ page.route("**/api/qmt/python-runtime/download", lambda route: (
188
+ submitted.append(route.request.post_data_json),
189
+ route.fulfill(json={"ok": True, "data": {"state": "queued"}})))
190
+ # The last progress event is not a terminal event. HTTP state/result must
191
+ # recover completion even if the WebSocket notification never arrived.
192
+ page.route("**/api/qmt/python-runtime/jobs?*", lambda route: route.fulfill(json={
193
+ "ok": True, "data": {"state": "done", "progress": {"phase": "installed", "percent": 100},
194
+ "result": {"account_config_required": True, "restarted": None}}}))
195
+ if socket_throws:
196
+ page.evaluate("() => { window.WebSocket = function() { throw new Error('WebSocket unavailable'); }; }")
197
+ page.locator("#bindingPythonRuntimeDownloadBtn").click()
198
+ page.wait_for_function("state.bindingPythonRuntimeJobId === '' && state.bindingPythonRuntimeProgress?.meta?.stage === 'done'")
199
+ assert len(submitted) == 1
200
+ assert page.evaluate("state.bindingPythonRuntimeDeferred")
201
+ assert not page.evaluate("state.bindingPythonRuntimeChoiceDirty")
202
+ expect(page.locator("#bindingPythonRuntimeProgressMeta")).to_contain_text("100%")
203
+ expect(page.locator("#bindingPythonRuntimeProgressText")).to_contain_text("登录")
124
204
  assert not errors