cfquant 0.2.67__tar.gz → 0.2.68__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.68}/PKG-INFO +1 -1
  2. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/__init__.py +2 -1
  3. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/client.py +2 -0
  4. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/monitor_notifications.py +3 -30
  5. cfquant-0.2.68/cfquant/notify.py +52 -0
  6. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/qmt_bridge.py +105 -30
  7. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/qmt_python_runtime.py +34 -10
  8. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_binding_python_runtime_browser.py +9 -2
  9. cfquant-0.2.68/cfquant/tests/test_binding_webhook_browser.py +106 -0
  10. cfquant-0.2.68/cfquant/tests/test_history_download_memory.py +68 -0
  11. cfquant-0.2.68/cfquant/tests/test_notify.py +214 -0
  12. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_qmt_python_runtime.py +41 -0
  13. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_trading_monitor.py +57 -0
  14. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_trading_monitor_browser.py +73 -0
  15. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/trading_monitor.py +16 -4
  16. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tx_trade_bridge.py +109 -30
  17. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/version.py +2 -2
  18. cfquant-0.2.68/cfquant/webhook.py +151 -0
  19. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/xtdata.py +2 -0
  20. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant.egg-info/PKG-INFO +1 -1
  21. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant.egg-info/SOURCES.txt +6 -0
  22. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant_web_server.py +99 -6
  23. {cfquant-0.2.67 → cfquant-0.2.68}/pyproject.toml +1 -1
  24. {cfquant-0.2.67 → cfquant-0.2.68}/qmt_scripts/CFQUANT_LITE.py +108 -31
  25. {cfquant-0.2.67 → cfquant-0.2.68}/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
  26. {cfquant-0.2.67 → cfquant-0.2.68}/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
  27. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/api-tester.js +1 -0
  28. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/app.js +148 -11
  29. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/index.html +44 -5
  30. cfquant-0.2.68/web_dashboard/notify-api-data.js +38 -0
  31. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/python-api.js +4 -4
  32. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/styles.css +8 -0
  33. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/trading-monitor.js +106 -16
  34. {cfquant-0.2.67 → cfquant-0.2.68}/LICENSE +0 -0
  35. {cfquant-0.2.67 → cfquant-0.2.68}/LTtx/__init__.py +0 -0
  36. {cfquant-0.2.67 → cfquant-0.2.68}/LTtx/tx/Config.txt +0 -0
  37. {cfquant-0.2.67 → cfquant-0.2.68}/LTtx/tx/LTtx_server.py +0 -0
  38. {cfquant-0.2.67 → cfquant-0.2.68}/LTtx/tx/__init__.py +0 -0
  39. {cfquant-0.2.67 → cfquant-0.2.68}/LTtx/tx/tx.py +0 -0
  40. {cfquant-0.2.67 → cfquant-0.2.68}/MANIFEST.in +0 -0
  41. {cfquant-0.2.67 → cfquant-0.2.68}/README.md +0 -0
  42. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/_editable_install.py +0 -0
  43. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/account_routing.py +0 -0
  44. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/algo.py +0 -0
  45. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/algo_broker.py +0 -0
  46. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/batch_orders.py +0 -0
  47. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/build_info.py +0 -0
  48. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/callback_dispatcher.py +0 -0
  49. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/cftrader.py +0 -0
  50. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/channels.py +0 -0
  51. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/cli.py +0 -0
  52. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/conditional_orders.py +0 -0
  53. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/config.py +0 -0
  54. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/diagnostic_context.py +0 -0
  55. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/diagnostics.py +0 -0
  56. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/flow_control.py +0 -0
  57. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/hub.py +0 -0
  58. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/level2.py +0 -0
  59. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/log_management.py +0 -0
  60. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/logging_i18n.py +0 -0
  61. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/management.py +0 -0
  62. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/monitor_performance.py +0 -0
  63. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/normal_bridge.py +0 -0
  64. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/order_identity.py +0 -0
  65. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/order_meta.py +0 -0
  66. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/pipe_bridge.py +0 -0
  67. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/pipe_client.py +0 -0
  68. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/pipe_hub.py +0 -0
  69. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/pipe_transport.py +0 -0
  70. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/planning_broker.py +0 -0
  71. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/position_sizing.py +0 -0
  72. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/protocol.py +0 -0
  73. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/qmt_binding_defaults.py +0 -0
  74. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/qmt_binding_settings.py +0 -0
  75. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/qmt_component_agent.js +0 -0
  76. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/qmt_component_login.py +0 -0
  77. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/qmt_credentials.py +0 -0
  78. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/qmt_dependencies.py +0 -0
  79. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/qmt_inner.py +0 -0
  80. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/qmt_inner_catalog.py +0 -0
  81. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/qmt_inner_runtime.py +0 -0
  82. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/qmt_mac_agent.js +0 -0
  83. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/qmt_native_files.py +0 -0
  84. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/qmt_native_worker.py +0 -0
  85. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/qmt_pipe_runtime.py +0 -0
  86. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/qmt_strategy_agent.js +0 -0
  87. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/qmt_strategy_deploy.py +0 -0
  88. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/qmt_strategy_package.py +0 -0
  89. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/qmt_strategy_recovery.py +0 -0
  90. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/qmt_strategy_runtime.py +0 -0
  91. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/qmt_update.py +0 -0
  92. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/qmt_vm_agent.js +0 -0
  93. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/qmt_vm_discovery.py +0 -0
  94. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/qmt_vm_runtime.py +0 -0
  95. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/qmt_vm_settings.py +0 -0
  96. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/qmt_vm_worker.py +0 -0
  97. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/quote_history.py +0 -0
  98. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/quote_leases.py +0 -0
  99. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/quote_service.py +0 -0
  100. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/remote_adapter.py +0 -0
  101. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/remote_management.py +0 -0
  102. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/remote_supervisor.py +0 -0
  103. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/rpc_timing.py +0 -0
  104. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/runtime_report.py +0 -0
  105. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/shared_quotes.py +0 -0
  106. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/smart_orders.py +0 -0
  107. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/stock_connect.py +0 -0
  108. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/subscription_bridge.py +0 -0
  109. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/subscription_client.py +0 -0
  110. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/subscription_manager.py +0 -0
  111. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/subscription_store.py +0 -0
  112. {cfquant-0.2.67 → cfquant-0.2.68}/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
  113. {cfquant-0.2.67 → cfquant-0.2.68}/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
  114. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/12_/350/256/242/345/215/225/345/244/207/346/263/250/346/265/213/350/257/225.py" +0 -0
  115. {cfquant-0.2.67 → cfquant-0.2.68}/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
  116. {cfquant-0.2.67 → cfquant-0.2.68}/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
  117. {cfquant-0.2.67 → cfquant-0.2.68}/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
  118. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/16_/345/220/257/345/212/250/345/256/211/350/243/205/346/265/213/350/257/225.py" +0 -0
  119. {cfquant-0.2.67 → cfquant-0.2.68}/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
  120. {cfquant-0.2.67 → cfquant-0.2.68}/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
  121. {cfquant-0.2.67 → cfquant-0.2.68}/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
  122. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/1_/350/241/214/346/203/205/346/216/245/346/224/266/346/265/213/350/257/225.py" +0 -0
  123. {cfquant-0.2.67 → cfquant-0.2.68}/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
  124. {cfquant-0.2.67 → cfquant-0.2.68}/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
  125. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/22_/350/277/236/346/216/245/350/257/212/346/226/255/346/265/213/350/257/225.py" +0 -0
  126. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/23_/347/244/272/344/276/213/351/205/215/347/275/256/346/265/213/350/257/225.py" +0 -0
  127. {cfquant-0.2.67 → cfquant-0.2.68}/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
  128. {cfquant-0.2.67 → cfquant-0.2.68}/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
  129. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/26_/345/205/250/346/216/250/350/256/242/351/230/205/346/265/213/350/257/225.py" +0 -0
  130. {cfquant-0.2.67 → cfquant-0.2.68}/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
  131. {cfquant-0.2.67 → cfquant-0.2.68}/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
  132. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/29_Level2/350/241/214/346/203/205/346/265/213/350/257/225.py" +0 -0
  133. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/2_/346/225/260/346/215/256/350/216/267/345/217/226/346/265/213/350/257/225.py" +0 -0
  134. {cfquant-0.2.67 → cfquant-0.2.68}/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
  135. {cfquant-0.2.67 → cfquant-0.2.68}/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
  136. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/32_/345/274/202/346/255/245/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  137. {cfquant-0.2.67 → cfquant-0.2.68}/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
  138. {cfquant-0.2.67 → cfquant-0.2.68}/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
  139. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/3_/346/225/260/346/215/256/344/270/213/350/275/275/346/265/213/350/257/225.py" +0 -0
  140. {cfquant-0.2.67 → cfquant-0.2.68}/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
  141. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/5_/347/234/237/345/256/236/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  142. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/6_/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
  143. {cfquant-0.2.67 → cfquant-0.2.68}/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
  144. {cfquant-0.2.67 → cfquant-0.2.68}/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
  145. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/9_/345/217/257/346/222/244/350/256/242/345/215/225/346/265/213/350/257/225.py" +0 -0
  146. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/_helpers.py +0 -0
  147. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/qmt_vm_native_fixture.js +0 -0
  148. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/readme.md +0 -0
  149. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_account_query_timing.py +0 -0
  150. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_account_subscription_probe.py +0 -0
  151. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_activity_log_browser.py +0 -0
  152. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_advanced_parity.py +0 -0
  153. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_algo.py +0 -0
  154. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_algo_broker.py +0 -0
  155. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_api_tester_browser.py +0 -0
  156. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_api_tester_web.py +0 -0
  157. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_auto_login_app.py +0 -0
  158. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
  159. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_binding_live_refresh_browser.py +0 -0
  160. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_binding_runtime_browser.py +0 -0
  161. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_binding_vm_browser.py +0 -0
  162. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_callback_recovery.py +0 -0
  163. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_callback_requests.py +0 -0
  164. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_cftrader.py +0 -0
  165. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_cftrader_tutorial.py +0 -0
  166. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_cftrader_web.py +0 -0
  167. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_credit_compat.py +0 -0
  168. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_dataframe_encoding.py +0 -0
  169. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_diagnostic_context.py +0 -0
  170. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_json_serialization.py +0 -0
  171. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_latency_flow_control.py +0 -0
  172. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_lite_account_routing.py +0 -0
  173. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_lite_deployment_paths.py +0 -0
  174. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_lite_parity.py +0 -0
  175. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_lite_qmt_dispatch.py +0 -0
  176. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_local_trading_calendar.py +0 -0
  177. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_log_management.py +0 -0
  178. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_management.py +0 -0
  179. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_management_batch.py +0 -0
  180. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_market_data_fallback.py +0 -0
  181. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_market_timing.py +0 -0
  182. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_monitor_notifications.py +0 -0
  183. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_monitor_performance.py +0 -0
  184. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_navigation_browser.py +0 -0
  185. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_order_identity.py +0 -0
  186. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_order_time_callback.py +0 -0
  187. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_pipe_shutdown.py +0 -0
  188. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_project_updater.py +0 -0
  189. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_python_api_reference.py +0 -0
  190. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_qmt_auto_login_browser.py +0 -0
  191. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_qmt_auto_login_integration.py +0 -0
  192. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_qmt_binding_defaults.py +0 -0
  193. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_qmt_binding_settings.py +0 -0
  194. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_qmt_component_login.py +0 -0
  195. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_qmt_dependencies.py +0 -0
  196. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_qmt_fileless.py +0 -0
  197. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_qmt_inner.py +0 -0
  198. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_qmt_inner_reference.py +0 -0
  199. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_qmt_inner_web.py +0 -0
  200. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_qmt_native_files.py +0 -0
  201. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_qmt_native_reuse.py +0 -0
  202. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_qmt_native_worker.py +0 -0
  203. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_qmt_package_import.py +0 -0
  204. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_qmt_strategy_browser.py +0 -0
  205. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_qmt_strategy_capabilities.py +0 -0
  206. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_qmt_strategy_deploy.py +0 -0
  207. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_qmt_strategy_recovery.py +0 -0
  208. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_qmt_strategy_web.py +0 -0
  209. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_qmt_update.py +0 -0
  210. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_qmt_vm_discovery.py +0 -0
  211. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_qmt_vm_native.py +0 -0
  212. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_qmt_vm_runtime.py +0 -0
  213. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_qmt_vm_settings.py +0 -0
  214. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_quote_latency.py +0 -0
  215. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_quote_latency_encoding.py +0 -0
  216. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_quote_service.py +0 -0
  217. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_remote_management.py +0 -0
  218. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_remote_management_browser.py +0 -0
  219. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_remote_version_check.py +0 -0
  220. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
  221. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_shared_quotes.py +0 -0
  222. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_shared_quotes_generated.py +0 -0
  223. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_smart_orders.py +0 -0
  224. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_smart_orders_api.py +0 -0
  225. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_smart_orders_docs.py +0 -0
  226. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_split_order_identity.py +0 -0
  227. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_startup_recovery.py +0 -0
  228. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_stock_connect.py +0 -0
  229. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_stock_connect_browser.py +0 -0
  230. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_strategy_recovery_api.py +0 -0
  231. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_subscription_congestion.py +0 -0
  232. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_subscription_persistence.py +0 -0
  233. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_table_conversion.py +0 -0
  234. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_trade_strategy_callback.py +0 -0
  235. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_trader_subscription_recovery.py +0 -0
  236. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_trading_dates.py +0 -0
  237. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_tutorial_reader.py +0 -0
  238. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_tx_auto_import.py +0 -0
  239. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_tx_shutdown.py +0 -0
  240. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_version_check.py +0 -0
  241. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tests/test_version_check_browser.py +0 -0
  242. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tools/THIRD_PARTY.md +0 -0
  243. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tools/upx/COPYING +0 -0
  244. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tools/upx/LICENSE +0 -0
  245. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tools/upx/NEWS +0 -0
  246. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tools/upx/README +0 -0
  247. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tools/upx/THANKS.txt +0 -0
  248. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tools/upx/upx-doc.html +0 -0
  249. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tools/upx/upx-doc.txt +0 -0
  250. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tools/upx/upx.1 +0 -0
  251. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tools/upx/upx.exe +0 -0
  252. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/trading_calendar.py +0 -0
  253. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/tx.py +0 -0
  254. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/xtconstant.py +0 -0
  255. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/xttrader.py +0 -0
  256. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant/xttype.py +0 -0
  257. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant.egg-info/dependency_links.txt +0 -0
  258. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant.egg-info/entry_points.txt +0 -0
  259. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant.egg-info/requires.txt +0 -0
  260. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant.egg-info/top_level.txt +0 -0
  261. {cfquant-0.2.67 → cfquant-0.2.68}/cfquant_pipe_hub.py +0 -0
  262. {cfquant-0.2.67 → cfquant-0.2.68}/qmt_scripts/CFQUANT.py +0 -0
  263. {cfquant-0.2.67 → cfquant-0.2.68}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +0 -0
  264. {cfquant-0.2.67 → cfquant-0.2.68}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
  265. {cfquant-0.2.67 → cfquant-0.2.68}/qmt_scripts/__init__.py +0 -0
  266. {cfquant-0.2.67 → cfquant-0.2.68}/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.68}/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.68}/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.68}/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.68}/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.68}/requirements.txt +0 -0
  272. {cfquant-0.2.67 → cfquant-0.2.68}/setup.cfg +0 -0
  273. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/__init__.py +0 -0
  274. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/activity-log.js +0 -0
  275. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/avatars/amber-pulse.svg +0 -0
  276. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/avatars/copper-grid.svg +0 -0
  277. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/avatars/market-blue.svg +0 -0
  278. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/avatars/rose-circuit.svg +0 -0
  279. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/avatars/signal-green.svg +0 -0
  280. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/avatars/slate-wave.svg +0 -0
  281. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/avatars/teal-orbit.svg +0 -0
  282. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/avatars/violet-node.svg +0 -0
  283. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/cfquant-architecture.html +0 -0
  284. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/cftrader-api-data.js +0 -0
  285. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/docs.js +0 -0
  286. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/favicon.svg +0 -0
  287. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
  288. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
  289. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
  290. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
  291. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
  292. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
  293. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
  294. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
  295. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
  296. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
  297. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
  298. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
  299. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
  300. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
  301. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
  302. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
  303. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
  304. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
  305. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/navigation.js +0 -0
  306. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/python-api-data.js +0 -0
  307. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/qmt-inner-api-data.js +0 -0
  308. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/remote-management.js +0 -0
  309. {cfquant-0.2.67 → cfquant-0.2.68}/web_dashboard/smart-order-api-data.js +0 -0
  310. {cfquant-0.2.67 → cfquant-0.2.68}/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.68
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,25 @@ 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
+ if restart and (not deferred_account_config or was_running):
638
659
  _callback(progress_callback, QmtPythonProgress(runtime, "restarting", message="正在重新启动 QMT"))
639
660
  if restart_callback:
640
661
  restarted = restart_callback()
@@ -646,7 +667,10 @@ class QmtPythonRuntime:
646
667
  restarted = None
647
668
  return {"runtime": runtime, "package": package.as_dict() if package else None,
648
669
  "archive": str(archive) if archive else None, "reused_installed": reuse,
649
- "installed": installed, "switched": switched, "restarted": restarted}
670
+ "installed": installed, "switched": switched, "restarted": restarted,
671
+ "account_config_required": deferred_account_config,
672
+ "message": ("运行库已安装;请先在 QMT 登录此账号,再重新检测并应用执行器。"
673
+ if deferred_account_config else "")}
650
674
 
651
675
  def _stop_for_switch(self):
652
676
  for process in self._qmt_processes():
@@ -119,6 +119,13 @@ def test_missing_account_config_shows_capabilities_but_requires_login(page, fron
119
119
  errors = open_runtime(page, frontend_url, runtime_status(config_exists=False))
120
120
  expect(page.locator("#bindingPythonRuntimeSelect option")).to_have_count(2)
121
121
  page.locator("#bindingPythonRuntimeSelect").select_option("py312")
122
- expect(page.locator("#bindingPythonRuntimeDownloadBtn")).to_be_disabled()
123
- expect(page.locator("#bindingPythonRuntimeChoiceHint")).to_contain_text("登录")
122
+ submitted = []
123
+ page.route("**/api/qmt/python-runtime/download", lambda route: (
124
+ submitted.append(route.request.post_data_json),
125
+ route.fulfill(json={"ok": True, "data": {"state": "queued"}})))
126
+ expect(page.locator("#bindingPythonRuntimeDownloadBtn")).to_be_enabled()
127
+ expect(page.locator("#bindingPythonRuntimeChoiceHint")).to_contain_text("先下载")
128
+ page.locator("#bindingPythonRuntimeDownloadBtn").click()
129
+ page.wait_for_function("state.bindingPythonRuntimeJobId")
130
+ assert submitted[0]["allow_missing_account_config"] is True
124
131
  assert not errors
@@ -0,0 +1,106 @@
1
+ """Account Webhook editing and docs on desktop/mobile; all APIs are offline."""
2
+ import ast
3
+ import inspect
4
+
5
+ from cfquant import notify
6
+ from cfquant.tests.test_tutorial_reader import browser, page, frontend_url, expect
7
+ from cfquant.tests.test_binding_runtime_browser import open_bindings
8
+
9
+
10
+ def test_new_binding_webhook_save_edit_preserves_credentials_and_clears(page, frontend_url, tmp_path):
11
+ _, errors = open_bindings(page, frontend_url)
12
+ page.locator('#openBindingDialogBtn').click()
13
+ page.locator('#bindingAccountId').fill('TEST_ONLY')
14
+ page.evaluate("fillQmtStrategySettings('binding', {enabled: false})")
15
+ page.locator('#bindingQmtAutoLogin').uncheck()
16
+ expect(page.locator('#bindingWebhookFields')).not_to_be_visible()
17
+ expect(page.locator('#bindingWebhookSaveBtn')).not_to_be_visible()
18
+ page.locator('#bindingWebhookEnabled').check()
19
+ page.locator('#bindingWebhookProvider').select_option('feishu')
20
+ page.locator('#bindingWebhookUrl').fill('https://example.invalid/PRIVATE_TOKEN')
21
+ page.locator('#bindingWebhookSecret').fill('PRIVATE_SECRET')
22
+ saved = []
23
+ rows = {}
24
+
25
+ def save(route):
26
+ body = route.request.post_data_json
27
+ saved.append(body)
28
+ row = dict(body, account_key='test:STOCK:TEST_ONLY', bridge_id='test', notification_webhook={
29
+ 'enabled': True, 'provider': 'feishu', 'url': '', 'secret': '',
30
+ 'url_configured': True, 'secret_configured': True})
31
+ rows[row['account_key']] = row
32
+ route.fulfill(json={'ok': True, 'data': {'account': row, 'account_configs': rows,
33
+ 'account_pairs': {}, 'bridges': {'test': {'name': 'Test'}},
34
+ 'qmt_strategy_deploy': {'enabled': False}, 'qmt_auto_login': {'enabled': False}}})
35
+
36
+ page.route('**/api/account-config', save)
37
+ page.locator('#bindingForm [type="submit"]').click()
38
+ expect(page.locator('#bindingDialogOverlay')).not_to_be_visible()
39
+ assert saved[0]['notification_webhook'] == {'enabled': True, 'provider': 'feishu',
40
+ 'url': 'https://example.invalid/PRIVATE_TOKEN', 'secret': 'PRIVATE_SECRET', 'clear_secret': False}
41
+ assert page.locator('#bindingWebhookUrl').input_value() == ''
42
+ page.evaluate("""() => {
43
+ document.querySelector('#bindingQmtGuideOverlay').classList.add('hidden');
44
+ openBindingDialog({accountKey: 'test:STOCK:TEST_ONLY'});
45
+ }""")
46
+ expect(page.locator('#bindingWebhookProvider')).to_have_value('feishu')
47
+ expect(page.locator('#bindingWebhookUrlHint')).to_contain_text('已配置')
48
+ expect(page.locator('#bindingWebhookSecret')).to_have_value('')
49
+ updates = []
50
+
51
+ def partial(route):
52
+ body = route.request.post_data_json
53
+ updates.append(body)
54
+ row = rows[body['account_key']]
55
+ if body['notification_webhook'].get('clear'):
56
+ row['notification_webhook'] = {'enabled': False, 'provider': 'wecom',
57
+ 'url_configured': False, 'secret_configured': False}
58
+ route.fulfill(json={'ok': True, 'data': {'account': row}})
59
+
60
+ page.route('**/api/account-config/webhook', partial)
61
+ page.locator('#bindingWebhookSaveBtn').click()
62
+ expect(page.locator('#bindingWebhookSaveStatus')).to_contain_text('已保存')
63
+ assert 'url' not in updates[0]['notification_webhook']
64
+ assert 'secret' not in updates[0]['notification_webhook']
65
+ assert len(saved) == 1
66
+ page.locator('#bindingWebhookProvider').select_option('dingtalk')
67
+ page.locator('#bindingWebhookSaveBtn').click()
68
+ expect(page.locator('#bindingSaveStatus')).to_contain_text('重新填写')
69
+ assert len(updates) == 1
70
+ page.locator('#bindingWebhookClearBtn').click()
71
+ page.locator('#bindingWebhookSaveBtn').click()
72
+ expect(page.locator('#bindingWebhookSaveStatus')).to_contain_text('已保存')
73
+ assert updates[-1]['notification_webhook'] == {'clear': True}
74
+ page.locator('#closeBindingDialogBtn').click()
75
+ page.mouse.move(page.viewport_size['width'] - 1, page.viewport_size['height'] - 1)
76
+ page.keyboard.press('Escape')
77
+ page.locator('#openBindingDialogBtn').click()
78
+ expect(page.locator('#bindingWebhookEnabled')).not_to_be_checked()
79
+ expect(page.locator('#bindingWebhookUrl')).to_have_value('')
80
+ page.locator('#bindingWebhookEnabled').check()
81
+ page.locator('#bindingWebhookSettings').scroll_into_view_if_needed()
82
+ page.screenshot(path=str(tmp_path / 'binding-webhook.png'))
83
+ assert page.locator('#bindingWebhookSettings').evaluate('node => node.scrollWidth <= node.clientWidth')
84
+ assert page.locator('#bindingDialogOverlay .binding-dialog').evaluate('node => node.scrollWidth <= node.clientWidth')
85
+ assert not errors
86
+
87
+
88
+ def test_notification_docs_are_discoverable_and_examples_match_sdk(page, frontend_url):
89
+ requests, errors = open_bindings(page, frontend_url)
90
+ page.locator('.nav-item[data-view="api"]').click()
91
+ group = page.locator('[data-api-group="system"]')
92
+ if group.get_attribute('aria-expanded') != 'true':
93
+ group.click()
94
+ page.locator('[data-endpoint-id="notification_send"]').click()
95
+ docs = page.locator('#apiDocDetail')
96
+ expect(docs).to_contain_text('资金账号字符串')
97
+ code = docs.locator('pre[data-language="python"]').inner_text()
98
+ for call in (node for node in ast.walk(ast.parse(code)) if isinstance(node, ast.Call)):
99
+ if isinstance(call.func, ast.Attribute) and call.func.attr == 'send':
100
+ inspect.signature(notify.send).bind(*[None for _ in call.args], **{kw.arg: None for kw in call.keywords})
101
+ page.locator('[data-api-sdk-tutorial]').click()
102
+ expect(page.locator('.python-document-head h2')).to_have_text('send')
103
+ expect(page.locator('.python-breadcrumb')).to_contain_text('消息推送')
104
+ assert page.locator('[data-python-cftrader-guide]').count() == 0
105
+ assert all(method == 'GET' for method, path in requests)
106
+ assert not errors
@@ -0,0 +1,68 @@
1
+ """Regression checks for bounded historical download requests."""
2
+
3
+ from types import SimpleNamespace
4
+
5
+ from cfquant.qmt_bridge import CfquantQmtBridge
6
+ from cfquant.tx_trade_bridge import TxTradeBridge
7
+
8
+
9
+ def _run_download(bridge_cls):
10
+ calls = []
11
+ events = []
12
+
13
+ def download(stock_list, period, start_time, end_time, callback, incrementally):
14
+ calls.append((list(stock_list), period, start_time, end_time, incrementally))
15
+ for index, stock_code in enumerate(stock_list, 1):
16
+ callback({
17
+ "finished": index,
18
+ "total": len(stock_list),
19
+ "stockcode": stock_code,
20
+ "message": "",
21
+ })
22
+
23
+ bridge = bridge_cls(
24
+ SimpleNamespace(),
25
+ show=False,
26
+ globals_dict={"download_history_data2": download},
27
+ )
28
+ bridge._send_event = lambda client, event, data, **kwargs: events.append((event, data, kwargs))
29
+ result = bridge._download_history_data2({
30
+ "stock_list": ["000001.SZ", "000002.SZ", "000003.SZ", "000004.SZ", "000005.SZ"],
31
+ "period": "1m",
32
+ "callback_event": "download:history",
33
+ "download_emit_lifecycle": True,
34
+ "download_batch_size": 2,
35
+ "incrementally": True,
36
+ }, {"client_id": "client-1"})
37
+ return result, calls, events
38
+
39
+
40
+ def test_tx_trade_history_download_is_serially_batched_and_progress_is_aggregated():
41
+ result, calls, events = _run_download(TxTradeBridge)
42
+
43
+ assert result is None
44
+ assert [call[0] for call in calls] == [
45
+ ["000001.SZ", "000002.SZ"],
46
+ ["000003.SZ", "000004.SZ"],
47
+ ["000005.SZ"],
48
+ ]
49
+ progress = [data for _, data, meta in events if meta["meta"]["stage"] == "progress"]
50
+ assert progress[-1]["finished"] == 5
51
+ assert progress[-1]["total"] == 5
52
+ request_done = [data for _, data, meta in events if meta["meta"]["stage"] == "request_done"]
53
+ assert len(request_done) == 1
54
+ assert request_done[0]["finished"] == 5
55
+
56
+
57
+ def test_legacy_qmt_bridge_uses_the_same_download_guard():
58
+ _, calls, _ = _run_download(CfquantQmtBridge)
59
+ assert [call[0] for call in calls] == [
60
+ ["000001.SZ", "000002.SZ"],
61
+ ["000003.SZ", "000004.SZ"],
62
+ ["000005.SZ"],
63
+ ]
64
+
65
+
66
+ def test_minute_download_defaults_to_a_small_batch():
67
+ assert TxTradeBridge._history_download_batch_size({"period": "1m"}) == 50
68
+ assert TxTradeBridge._history_download_batch_size({"period": "1d"}) == 200