cfquant 0.2.51__tar.gz → 0.2.52__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 (187) hide show
  1. {cfquant-0.2.51 → cfquant-0.2.52}/PKG-INFO +1 -1
  2. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/client.py +9 -5
  3. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/level2.py +7 -1
  4. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/normal_bridge.py +3 -2
  5. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/pipe_bridge.py +6 -2
  6. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/pipe_client.py +9 -5
  7. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/protocol.py +65 -18
  8. cfquant-0.2.52/cfquant/qmt_binding_settings.py +39 -0
  9. cfquant-0.2.52/cfquant/rpc_timing.py +45 -0
  10. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/28_Level2/346/216/245/345/217/243/351/200/202/351/205/215/346/265/213/350/257/225.py +6 -3
  11. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_api_tester_browser.py +103 -8
  12. cfquant-0.2.52/cfquant/tests/test_binding_runtime_browser.py +109 -0
  13. cfquant-0.2.52/cfquant/tests/test_dataframe_encoding.py +192 -0
  14. cfquant-0.2.52/cfquant/tests/test_lite_qmt_dispatch.py +266 -0
  15. cfquant-0.2.52/cfquant/tests/test_market_timing.py +238 -0
  16. cfquant-0.2.52/cfquant/tests/test_qmt_binding_settings.py +123 -0
  17. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_qmt_fileless.py +14 -3
  18. cfquant-0.2.52/cfquant/tests/test_table_conversion.py +122 -0
  19. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tx_trade_bridge.py +91 -44
  20. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/version.py +2 -2
  21. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant.egg-info/PKG-INFO +1 -1
  22. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant.egg-info/SOURCES.txt +8 -0
  23. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant_web_server.py +109 -28
  24. {cfquant-0.2.51 → cfquant-0.2.52}/pyproject.toml +1 -1
  25. {cfquant-0.2.51 → cfquant-0.2.52}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +33 -5
  26. {cfquant-0.2.51 → cfquant-0.2.52}/qmt_scripts/CFQUANT_LITE.py +298 -62
  27. {cfquant-0.2.51 → cfquant-0.2.52}/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 +33 -5
  28. {cfquant-0.2.51 → cfquant-0.2.52}/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 +33 -5
  29. {cfquant-0.2.51 → cfquant-0.2.52}/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 +298 -62
  30. {cfquant-0.2.51 → cfquant-0.2.52}/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 +298 -62
  31. {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/app.js +104 -6
  32. {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/index.html +24 -2
  33. {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/styles.css +58 -15
  34. {cfquant-0.2.51 → cfquant-0.2.52}/LICENSE +0 -0
  35. {cfquant-0.2.51 → cfquant-0.2.52}/LTtx/__init__.py +0 -0
  36. {cfquant-0.2.51 → cfquant-0.2.52}/LTtx/tx/Config.txt +0 -0
  37. {cfquant-0.2.51 → cfquant-0.2.52}/LTtx/tx/LTtx_server.py +0 -0
  38. {cfquant-0.2.51 → cfquant-0.2.52}/LTtx/tx/__init__.py +0 -0
  39. {cfquant-0.2.51 → cfquant-0.2.52}/LTtx/tx/tx.py +0 -0
  40. {cfquant-0.2.51 → cfquant-0.2.52}/MANIFEST.in +0 -0
  41. {cfquant-0.2.51 → cfquant-0.2.52}/README.md +0 -0
  42. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/__init__.py +0 -0
  43. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/_editable_install.py +0 -0
  44. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/account_routing.py +0 -0
  45. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/batch_orders.py +0 -0
  46. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/build_info.py +0 -0
  47. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/callback_dispatcher.py +0 -0
  48. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/cftrader.py +0 -0
  49. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/channels.py +0 -0
  50. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/cli.py +0 -0
  51. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/config.py +0 -0
  52. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/hub.py +0 -0
  53. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/log_management.py +0 -0
  54. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/logging_i18n.py +0 -0
  55. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/management.py +0 -0
  56. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/order_identity.py +0 -0
  57. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/order_meta.py +0 -0
  58. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/pipe_hub.py +0 -0
  59. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/pipe_transport.py +0 -0
  60. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/qmt_bridge.py +0 -0
  61. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/qmt_pipe_runtime.py +0 -0
  62. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/qmt_strategy_deploy.py +0 -0
  63. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/qmt_strategy_package.py +0 -0
  64. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/qmt_strategy_runtime.py +0 -0
  65. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/runtime_report.py +0 -0
  66. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/stock_connect.py +0 -0
  67. {cfquant-0.2.51 → cfquant-0.2.52}/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
  68. {cfquant-0.2.51 → cfquant-0.2.52}/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
  69. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/12_/350/256/242/345/215/225/345/244/207/346/263/250/346/265/213/350/257/225.py" +0 -0
  70. {cfquant-0.2.51 → cfquant-0.2.52}/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
  71. {cfquant-0.2.51 → cfquant-0.2.52}/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
  72. {cfquant-0.2.51 → cfquant-0.2.52}/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
  73. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/16_/345/220/257/345/212/250/345/256/211/350/243/205/346/265/213/350/257/225.py" +0 -0
  74. {cfquant-0.2.51 → cfquant-0.2.52}/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
  75. {cfquant-0.2.51 → cfquant-0.2.52}/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
  76. {cfquant-0.2.51 → cfquant-0.2.52}/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
  77. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/1_/350/241/214/346/203/205/346/216/245/346/224/266/346/265/213/350/257/225.py" +0 -0
  78. {cfquant-0.2.51 → cfquant-0.2.52}/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
  79. {cfquant-0.2.51 → cfquant-0.2.52}/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
  80. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/22_/350/277/236/346/216/245/350/257/212/346/226/255/346/265/213/350/257/225.py" +0 -0
  81. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/23_/347/244/272/344/276/213/351/205/215/347/275/256/346/265/213/350/257/225.py" +0 -0
  82. {cfquant-0.2.51 → cfquant-0.2.52}/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
  83. {cfquant-0.2.51 → cfquant-0.2.52}/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
  84. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/26_/345/205/250/346/216/250/350/256/242/351/230/205/346/265/213/350/257/225.py" +0 -0
  85. {cfquant-0.2.51 → cfquant-0.2.52}/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
  86. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/29_Level2/350/241/214/346/203/205/346/265/213/350/257/225.py" +0 -0
  87. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/2_/346/225/260/346/215/256/350/216/267/345/217/226/346/265/213/350/257/225.py" +0 -0
  88. {cfquant-0.2.51 → cfquant-0.2.52}/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
  89. {cfquant-0.2.51 → cfquant-0.2.52}/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
  90. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/32_/345/274/202/346/255/245/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  91. {cfquant-0.2.51 → cfquant-0.2.52}/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
  92. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/3_/346/225/260/346/215/256/344/270/213/350/275/275/346/265/213/350/257/225.py" +0 -0
  93. {cfquant-0.2.51 → cfquant-0.2.52}/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
  94. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/5_/347/234/237/345/256/236/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  95. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/6_/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
  96. {cfquant-0.2.51 → cfquant-0.2.52}/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
  97. {cfquant-0.2.51 → cfquant-0.2.52}/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
  98. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/9_/345/217/257/346/222/244/350/256/242/345/215/225/346/265/213/350/257/225.py" +0 -0
  99. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/_helpers.py +0 -0
  100. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/log/cftrader/346/250/241/346/213/237/350/264/246/345/217/267/346/211/271/351/207/217/344/270/213/345/215/225/346/265/213/350/257/225_20260911_024617.md" +0 -0
  101. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/log/cftrader/346/250/241/346/213/237/350/264/246/345/217/267/346/211/271/351/207/217/344/270/213/345/215/225/346/265/213/350/257/225_20260921_175732.md" +0 -0
  102. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/readme.md +0 -0
  103. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_advanced_parity.py +0 -0
  104. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_api_tester_web.py +0 -0
  105. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
  106. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_callback_requests.py +0 -0
  107. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_cftrader.py +0 -0
  108. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_cftrader_tutorial.py +0 -0
  109. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_cftrader_web.py +0 -0
  110. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_credit_compat.py +0 -0
  111. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_json_serialization.py +0 -0
  112. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_lite_account_routing.py +0 -0
  113. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_lite_deployment_paths.py +0 -0
  114. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_lite_parity.py +0 -0
  115. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_local_trading_calendar.py +0 -0
  116. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_log_management.py +0 -0
  117. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_management.py +0 -0
  118. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_management_batch.py +0 -0
  119. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_market_data_fallback.py +0 -0
  120. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_order_identity.py +0 -0
  121. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_order_time_callback.py +0 -0
  122. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_project_updater.py +0 -0
  123. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_python_api_reference.py +0 -0
  124. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_qmt_strategy_browser.py +0 -0
  125. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_qmt_strategy_deploy.py +0 -0
  126. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_qmt_strategy_web.py +0 -0
  127. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
  128. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_startup_recovery.py +0 -0
  129. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_stock_connect.py +0 -0
  130. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_stock_connect_browser.py +0 -0
  131. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_trade_strategy_callback.py +0 -0
  132. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_trading_dates.py +0 -0
  133. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_tutorial_reader.py +0 -0
  134. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tests/test_tx_auto_import.py +0 -0
  135. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/trading_calendar.py +0 -0
  136. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/tx.py +0 -0
  137. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/xtconstant.py +0 -0
  138. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/xtdata.py +0 -0
  139. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/xttrader.py +0 -0
  140. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant/xttype.py +0 -0
  141. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant.egg-info/dependency_links.txt +0 -0
  142. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant.egg-info/entry_points.txt +0 -0
  143. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant.egg-info/requires.txt +0 -0
  144. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant.egg-info/top_level.txt +0 -0
  145. {cfquant-0.2.51 → cfquant-0.2.52}/cfquant_pipe_hub.py +0 -0
  146. {cfquant-0.2.51 → cfquant-0.2.52}/qmt_scripts/CFQUANT.py +0 -0
  147. {cfquant-0.2.51 → cfquant-0.2.52}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
  148. {cfquant-0.2.51 → cfquant-0.2.52}/qmt_scripts/__init__.py +0 -0
  149. {cfquant-0.2.51 → cfquant-0.2.52}/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
  150. {cfquant-0.2.51 → cfquant-0.2.52}/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
  151. {cfquant-0.2.51 → cfquant-0.2.52}/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
  152. {cfquant-0.2.51 → cfquant-0.2.52}/requirements.txt +0 -0
  153. {cfquant-0.2.51 → cfquant-0.2.52}/setup.cfg +0 -0
  154. {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/__init__.py +0 -0
  155. {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/api-tester.js +0 -0
  156. {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/avatars/amber-pulse.svg +0 -0
  157. {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/avatars/copper-grid.svg +0 -0
  158. {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/avatars/market-blue.svg +0 -0
  159. {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/avatars/rose-circuit.svg +0 -0
  160. {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/avatars/signal-green.svg +0 -0
  161. {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/avatars/slate-wave.svg +0 -0
  162. {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/avatars/teal-orbit.svg +0 -0
  163. {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/avatars/violet-node.svg +0 -0
  164. {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/cftrader-api-data.js +0 -0
  165. {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/docs.js +0 -0
  166. {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/favicon.svg +0 -0
  167. {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
  168. {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
  169. {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
  170. {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
  171. {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
  172. {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
  173. {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
  174. {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
  175. {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
  176. {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
  177. {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
  178. {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
  179. {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
  180. {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
  181. {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
  182. {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
  183. {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
  184. {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
  185. {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/python-api-data.js +0 -0
  186. {cfquant-0.2.51 → cfquant-0.2.52}/web_dashboard/python-api.js +0 -0
  187. {cfquant-0.2.51 → cfquant-0.2.52}/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.51
3
+ Version: 0.2.52
4
4
  Summary: QMT local bridge and xtquant-compatible Python client.
5
5
  License: MIT
6
6
  Requires-Python: >=3.8
@@ -7,7 +7,8 @@ import time
7
7
 
8
8
  from .config import get_config
9
9
  from .callback_dispatcher import CallbackDispatcher, dispatch_callbacks
10
- from .protocol import decode_value, dumps_message, loads_message, new_id, pack_request
10
+ from .protocol import decode_value, dumps_message, loads_message, new_id, pack_request
11
+ from .rpc_timing import RpcTiming
11
12
 
12
13
 
13
14
  class CfquantError(RuntimeError):
@@ -155,7 +156,7 @@ class LTtxRpcClient(object):
155
156
  pass
156
157
  self._pending.clear()
157
158
 
158
- def request(self, action, params=None, timeout=None, request_channel=None):
159
+ def request(self, action, params=None, timeout=None, request_channel=None, response_meta=None):
159
160
  effective_timeout = float(timeout or self.timeout)
160
161
  request_id = new_id("req")
161
162
  raw = pack_request(
@@ -167,6 +168,7 @@ class LTtxRpcClient(object):
167
168
  timeout=effective_timeout,
168
169
  )
169
170
  q = queue.Queue(maxsize=1)
171
+ timing = RpcTiming(response_meta, request_id)
170
172
  try:
171
173
  with self._lock:
172
174
  self.start()
@@ -177,11 +179,13 @@ class LTtxRpcClient(object):
177
179
  msg = q.get(timeout=effective_timeout)
178
180
  except queue.Empty:
179
181
  raise CfquantTimeout("cfquant request timeout: %s" % action)
180
- if not msg.get("ok"):
182
+ timing.received(msg)
183
+ if not msg.get("ok"):
181
184
  err = msg.get("error") or {}
182
185
  raise CfquantError(err.get("message") or str(err))
183
- return decode_value(msg.get("result"))
184
- finally:
186
+ return timing.decode(msg.get("result"), decode_value)
187
+ finally:
188
+ timing.finish()
185
189
  with self._pending_lock:
186
190
  self._pending.pop(request_id, None)
187
191
 
@@ -1,5 +1,7 @@
1
1
  """Level2 data adapters shared by the SDK and QMT bridges."""
2
2
 
3
+ from .protocol import dataframe_numeric_rows
4
+
3
5
  L2_PERIODS = (
4
6
  "l2quote", "l2quoteaux", "l2order", "l2transaction",
5
7
  "l2transactioncount", "l2orderqueue",
@@ -93,7 +95,11 @@ def quote_plain(value):
93
95
 
94
96
  def quote_records(value):
95
97
  if hasattr(value, "columns") and hasattr(value, "to_dict"):
96
- # DataFrame.values can round large integer IDs in a mixed float/int table.
98
+ rows = dataframe_numeric_rows(value)
99
+ if rows is not None:
100
+ columns = list(value.columns)
101
+ return quote_plain([dict(zip(columns, row)) for row in rows])
102
+ # Keep pandas' scalar boxing for nullable, date-like and object fields.
97
103
  return quote_plain(value.to_dict("records"))
98
104
  names = getattr(getattr(value, "dtype", None), "names", None)
99
105
  if names:
@@ -184,6 +184,7 @@ class NormalQmtBridge(TxTradeBridge):
184
184
  msg = loads_message(raw)
185
185
  if not msg or msg.get("type") != "request":
186
186
  return
187
+ msg["_received_monotonic"] = time.perf_counter()
187
188
  action = msg.get("action")
188
189
  if action == "cfquant.ping":
189
190
  self._send_response(msg, {"pong": True, "ts": time.time(), "request_channel": self.request_channel})
@@ -840,7 +841,7 @@ class NormalQmtBridge(TxTradeBridge):
840
841
  client_id = msg.get("client_id") or msg.get("reply_channel")
841
842
  if not client_id:
842
843
  return
843
- response = pack_response(msg.get("id"), ok=True, result=result)
844
+ response = pack_response(msg.get("id"), ok=True, result=result, meta=msg.get("_response_meta"))
844
845
  self.tx.push("response", response, client_id)
845
846
 
846
847
  def _send_error(self, msg, error):
@@ -851,7 +852,7 @@ class NormalQmtBridge(TxTradeBridge):
851
852
  "normal bridge send_error action=%s id=%s client_id=%s error=%s"
852
853
  % (msg.get("action"), msg.get("id"), client_id, error)
853
854
  )
854
- response = pack_response(msg.get("id"), ok=False, error=error)
855
+ response = pack_response(msg.get("id"), ok=False, error=error, meta=msg.get("_response_meta"))
855
856
  self.tx.push("response", response, client_id)
856
857
 
857
858
  def publish_callback_event(self, event_name, obj):
@@ -66,7 +66,9 @@ class PipeNormalQmtBridge(NormalQmtBridge):
66
66
  endpoint_name="normal",
67
67
  show=self.show,
68
68
  connect_timeout_ms=self.connect_timeout_ms,
69
- ).start()
69
+ )
70
+ self.tx._log = self._log
71
+ self.tx.start()
70
72
  self.tx.start_txg(self.request_channel)
71
73
  self.recv_thread = threading.Thread(target=self._recv_loop)
72
74
  self.recv_thread.daemon = True
@@ -153,7 +155,9 @@ class PipeTradeBridge(TxTradeBridge):
153
155
  endpoint_name="trade",
154
156
  show=self.show,
155
157
  connect_timeout_ms=self.connect_timeout_ms,
156
- ).start()
158
+ )
159
+ self.tx._log = self._log
160
+ self.tx.start()
157
161
  self.tx.start_txg(self.request_channel)
158
162
  self._log(
159
163
  "pipe trade bridge started pipe=%s request_channel=%s"
@@ -11,7 +11,8 @@ from .pipe_transport import (
11
11
  loads_pipe_message,
12
12
  normalize_pipe_name,
13
13
  )
14
- from .protocol import decode_value, dumps_message, loads_message, new_id, pack_request
14
+ from .protocol import decode_value, dumps_message, loads_message, new_id, pack_request
15
+ from .rpc_timing import RpcTiming
15
16
 
16
17
 
17
18
  class PipeRpcClient(object):
@@ -70,7 +71,7 @@ class PipeRpcClient(object):
70
71
  self._close_conns_locked()
71
72
  self._fail_pending("cfquant pipe client closed")
72
73
 
73
- def request(self, action, params=None, timeout=None, request_channel=None):
74
+ def request(self, action, params=None, timeout=None, request_channel=None, response_meta=None):
74
75
  effective_timeout = float(timeout or self.timeout)
75
76
  request_id = new_id("req")
76
77
  raw = pack_request(
@@ -82,6 +83,7 @@ class PipeRpcClient(object):
82
83
  timeout=effective_timeout,
83
84
  )
84
85
  q = queue.Queue(maxsize=1)
86
+ timing = RpcTiming(response_meta, request_id)
85
87
  try:
86
88
  with self._lock:
87
89
  self.start()
@@ -98,13 +100,15 @@ class PipeRpcClient(object):
98
100
  from .client import CfquantTimeout
99
101
 
100
102
  raise CfquantTimeout("cfquant pipe request timeout: %s" % action)
101
- if not msg.get("ok"):
103
+ timing.received(msg)
104
+ if not msg.get("ok"):
102
105
  err = msg.get("error") or {}
103
106
  from .client import CfquantError
104
107
 
105
108
  raise CfquantError(err.get("message") or str(err))
106
- return decode_value(msg.get("result"))
107
- finally:
109
+ return timing.decode(msg.get("result"), decode_value)
110
+ finally:
111
+ timing.finish()
108
112
  with self._pending_lock:
109
113
  self._pending.pop(request_id, None)
110
114
 
@@ -80,8 +80,8 @@ def loads_message(raw):
80
80
  raw = raw.decode("utf-8", errors="replace")
81
81
  if not isinstance(raw, str):
82
82
  return None
83
- if "|" in raw:
84
- _, raw = raw.split("|", 1)
83
+ if not raw.startswith(MESSAGE_PREFIX) and "|" in raw:
84
+ _, raw = raw.split("|", 1)
85
85
  if not raw.startswith(MESSAGE_PREFIX):
86
86
  return None
87
87
  try:
@@ -107,14 +107,20 @@ def pack_request(action, params=None, reply_channel=None, client_id=None, reques
107
107
  return dumps_message(payload)
108
108
 
109
109
 
110
- def pack_response(request_id, ok=True, result=None, error=None, meta=None):
111
- return dumps_message({
110
+ def pack_response(request_id, ok=True, result=None, error=None, meta=None):
111
+ started = time.perf_counter()
112
+ encoded = encode_value(result)
113
+ meta = dict(meta or {})
114
+ timing = meta.get("timing")
115
+ if isinstance(timing, dict) and isinstance(timing.get("qmt"), dict):
116
+ timing["qmt"]["encode_ms"] = round((time.perf_counter() - started) * 1000, 3)
117
+ return dumps_message({
112
118
  "type": "response",
113
119
  "id": request_id,
114
120
  "ok": bool(ok),
115
- "result": encode_value(result),
121
+ "result": encoded,
116
122
  "error": encode_error(error),
117
- "meta": meta or {},
123
+ "meta": meta,
118
124
  })
119
125
 
120
126
 
@@ -212,25 +218,60 @@ def _looks_like_series(value):
212
218
  )
213
219
 
214
220
 
215
- def _clean_cell(value):
221
+ def _clean_cell(value):
216
222
  if value is None:
217
223
  return None
218
224
  if isinstance(value, float) and (math.isnan(value) or math.isinf(value)):
219
225
  return None
220
- return encode_value(value)
221
-
222
-
226
+ return encode_value(value)
227
+
228
+
229
+ def dataframe_numeric_rows(value):
230
+ """Bulk rows for plain numeric frames; None keeps pandas' other semantics."""
231
+ columns = getattr(value, "columns", None)
232
+ dtypes = getattr(value, "dtypes", None)
233
+ if columns is None or not len(columns) or dtypes is None or not getattr(columns, "is_unique", False):
234
+ return None
235
+ # Nullable, object and date-like columns need pandas' scalar boxing rules.
236
+ # Restrict this fast path so callers keep their existing null/date behavior.
237
+ for dtype in dtypes:
238
+ if (type(dtype).__module__.split(".", 1)[0] != "numpy"
239
+ or dtype.kind not in ("b", "i", "u", "f") or dtype.itemsize > 8):
240
+ return None
241
+ # Combining mixed numeric blocks before this cast can round large IDs.
242
+ return value.astype(object, copy=False).values.tolist()
243
+
244
+
223
245
  def _encode_dataframe(value):
224
246
  try:
225
- raw_rows = list(value.itertuples(index=False, name=None))
247
+ # itertuples builds a Series per column, which is expensive for QMT's
248
+ # stock-by-date history frames. Cast BEFORE combining the blocks: using
249
+ # values directly can round int64/uint64 IDs in mixed float/int frames.
250
+ # astype also works with QMT's pandas 0.22 (which has no to_numpy).
251
+ raw_rows = value.astype(object, copy=False).values.tolist() if len(value.columns) else []
226
252
  except Exception:
227
253
  try:
228
- raw_rows = value.values.tolist()
254
+ raw_rows = list(value.itertuples(index=False, name=None))
229
255
  except Exception:
230
- raw_rows = []
231
- rows = []
232
- for row in raw_rows:
233
- rows.append([_clean_cell(v) for v in row])
256
+ try:
257
+ raw_rows = value.values.tolist()
258
+ except Exception:
259
+ raw_rows = []
260
+ rows = []
261
+ for row in raw_rows:
262
+ rows.append([_clean_cell(v) for v in row])
263
+ # Thousands of date columns usually share one dtype. Formatting its name
264
+ # repeatedly can cost more than extracting the actual values.
265
+ dtype_flags = {}
266
+ object_columns = []
267
+ for name, dtype in getattr(value, "dtypes", {}).items():
268
+ is_object = dtype_flags.get(dtype)
269
+ if is_object is None:
270
+ dtype_name = str(dtype)
271
+ is_object = dtype_name == "object" or dtype_name.startswith(("Int", "UInt"))
272
+ dtype_flags[dtype] = is_object
273
+ if is_object:
274
+ object_columns.append(_encode_label(name))
234
275
  index_name = getattr(getattr(value, "index", None), "name", None)
235
276
  return {
236
277
  "__cf_type__": "dataframe",
@@ -238,8 +279,7 @@ def _encode_dataframe(value):
238
279
  "index": [_encode_label(i) for i in getattr(value, "index", [])],
239
280
  "data": rows,
240
281
  "index_name": _encode_label(index_name) if index_name is not None else None,
241
- "object_columns": [_encode_label(name) for name, dtype in getattr(value, "dtypes", {}).items()
242
- if str(dtype) == "object" or str(dtype).startswith(("Int", "UInt"))],
282
+ "object_columns": object_columns,
243
283
  }
244
284
 
245
285
 
@@ -328,6 +368,13 @@ def decode_value(value):
328
368
  parts[2] = tuple(parts[2])
329
369
  dtype_descr.append(tuple(parts))
330
370
  dtype = np.dtype(dtype_descr)
371
+ # JSON turns each structured record tuple into a list. NumPy
372
+ # treats that list as another array dimension unless restored.
373
+ def record_tuples(items, dimensions):
374
+ if dimensions == 0:
375
+ return tuple(items)
376
+ return [record_tuples(item, dimensions - 1) for item in items]
377
+ data = record_tuples(data, len(value.get("shape", [len(data)])))
331
378
  elif value.get("dtype"):
332
379
  try:
333
380
  dtype = np.dtype(value.get("dtype"))
@@ -0,0 +1,39 @@
1
+ """Persisted per-binding QMT scheduling and log preferences."""
2
+ DEFAULT_TIMER_INTERVAL_MS = 1
3
+ MIN_TIMER_INTERVAL_MS = 1
4
+ MAX_TIMER_INTERVAL_MS = 1000
5
+
6
+
7
+ def normalize_qmt_runtime_settings(value=None, existing=None, strict=False):
8
+ if value is not None and not isinstance(value, dict):
9
+ if strict:
10
+ raise ValueError("QMT runtime settings must be an object")
11
+ value = None
12
+ data = dict(existing or {})
13
+ data.update(value or {})
14
+ interval = data.get("timer_interval_ms", DEFAULT_TIMER_INTERVAL_MS)
15
+ try:
16
+ if isinstance(interval, bool) or str(interval).strip() != str(int(interval)):
17
+ raise ValueError()
18
+ interval = int(interval)
19
+ if not MIN_TIMER_INTERVAL_MS <= interval <= MAX_TIMER_INTERVAL_MS:
20
+ raise ValueError()
21
+ except (TypeError, ValueError, OverflowError):
22
+ if strict:
23
+ raise ValueError("QMT 调度间隔必须是 1–1000 ms 的整数")
24
+ interval = DEFAULT_TIMER_INTERVAL_MS
25
+ enabled = data.get("log_enabled") # None preserves legacy global preferences.
26
+ if enabled is not None and not isinstance(enabled, bool):
27
+ if strict:
28
+ raise ValueError("QMT 日志设置必须为 true、false 或 null(跟随全局)")
29
+ enabled = None
30
+ return {"timer_interval_ms": interval, "log_enabled": enabled}
31
+
32
+
33
+ def qmt_runtime_identity(row, global_log_enabled=True):
34
+ settings = normalize_qmt_runtime_settings((row or {}).get("qmt_runtime"))
35
+ enabled = settings["log_enabled"]
36
+ if (row or {}).get("mode") == "lttx":
37
+ enabled = None # Advanced entries keep their existing global controls.
38
+ return {"qmt_timer_interval_ms": settings["timer_interval_ms"],
39
+ "qmt_log_enabled": bool(global_log_enabled) if enabled is None else enabled}
@@ -0,0 +1,45 @@
1
+ """Per-request diagnostics; never change the RPC method's return value."""
2
+ import time
3
+
4
+
5
+ class RpcTiming(object):
6
+ def __init__(self, output, request_id):
7
+ self.output = output
8
+ self.started = time.perf_counter()
9
+ self.received_at = None
10
+ if output is not None:
11
+ output.update({
12
+ "request_id": request_id,
13
+ "qmt": {"available": False, "reason": "no_response"},
14
+ "rpc": {"response_received": False},
15
+ })
16
+
17
+ def received(self, message):
18
+ self.received_at = time.perf_counter()
19
+ if self.output is None:
20
+ return
21
+ self.output["rpc"].update({
22
+ "response_received": True,
23
+ "response_ok": bool(message.get("ok")),
24
+ "wait_ms": round((self.received_at - self.started) * 1000, 3),
25
+ })
26
+ meta = message.get("meta") or {}
27
+ qmt = (meta.get("timing") or {}).get("qmt")
28
+ self.output["qmt"] = dict(qmt) if isinstance(qmt, dict) else {
29
+ "available": False, "reason": "bridge_timing_unavailable",
30
+ }
31
+
32
+ def decode(self, value, decoder):
33
+ started = time.perf_counter()
34
+ try:
35
+ return decoder(value)
36
+ finally:
37
+ if self.output is not None:
38
+ self.output["rpc"]["decode_ms"] = round((time.perf_counter() - started) * 1000, 3)
39
+
40
+ def finish(self):
41
+ if self.output is not None:
42
+ elapsed = round((time.perf_counter() - self.started) * 1000, 3)
43
+ self.output["rpc"]["total_ms"] = elapsed
44
+ if self.received_at is None:
45
+ self.output["rpc"]["wait_ms"] = elapsed
@@ -32,7 +32,8 @@ CODE = "000001.SZ"
32
32
  def lite_namespace(path):
33
33
  tree = ast.parse(path.read_text(encoding="gbk"))
34
34
  helpers = {key for key in vars(level2) if not key.startswith("__") and key != "l2_array"}
35
- functions = {"encode_value", "_encode_dataframe", "_encode_series", "_clean_cell", "_looks_like_dataframe", "_looks_like_series", "_get_pandas"}
35
+ functions = {"encode_value", "_encode_dataframe", "_encode_series", "_encode_label", "_encode_ndarray",
36
+ "_clean_cell", "_looks_like_dataframe", "_looks_like_series", "_get_pandas"}
36
37
  body = []
37
38
  for node in tree.body:
38
39
  if isinstance(node, ast.FunctionDef) and node.name in helpers | functions:
@@ -143,7 +144,8 @@ def test_sdk_l2_queries_preserve_fields_precision_and_depth(monkeypatch, bridge,
143
144
  def test_six_period_query_protocol_and_no_forward_fill(bridge, period):
144
145
  result = rpc(bridge, "xtdata.get_market_data_ex", {"stock_list": [CODE], "period": period, "fill_data": True})
145
146
  assert result[CODE]["orderNo"].iloc[0] == BIG_ID
146
- assert result[CODE]["bidPrice"].iloc[0] == [11.6, 11.5]
147
+ assert isinstance(result[CODE]["bidPrice"].iloc[0], np.ndarray)
148
+ np.testing.assert_array_equal(result[CODE]["bidPrice"].iloc[0], [11.6, 11.5])
147
149
  assert bridge.context.calls[-1][2] == period
148
150
  assert bridge.context.calls[-1][-1] is False
149
151
  rpc(bridge, "xtdata.get_market_data", {"stock_list": [CODE], "period": period})
@@ -254,7 +256,8 @@ def test_wire_encoder_preserves_nullable_ids_arrays_and_numpy_scalars(path):
254
256
  result = decode_value(json.loads(json.dumps(encoder(frame))))
255
257
  assert result["id"].iloc[0] == BIG_ID
256
258
  assert result["id"].iloc[1] is None
257
- assert result["depth"].iloc[0] == [100, 200]
259
+ assert isinstance(result["depth"].iloc[0], np.ndarray)
260
+ np.testing.assert_array_equal(result["depth"].iloc[0], [100, 200])
258
261
  assert encoder(np.uint64(2 ** 64 - 1)) == 2 ** 64 - 1
259
262
 
260
263
 
@@ -16,8 +16,8 @@ def open_test(page, frontend_url, entry):
16
16
  requests, errors = open_app(page, frontend_url)
17
17
  page.locator('#setupOverlay [data-open-tutorial]').click()
18
18
  page.evaluate('(id) => window.CfquantPythonReference.open(id)', entry)
19
- page.locator('[data-python-test]').click()
20
- panel = page.locator('.api-inline-test:not([hidden])')
19
+ page.locator('#tutorialReader [data-python-test]').click()
20
+ panel = page.locator('#tutorialReader .api-inline-test:not([hidden])')
21
21
  expect(panel).to_be_visible()
22
22
  assert all(method == 'GET' for method, path in requests)
23
23
  return panel, requests, errors
@@ -36,7 +36,7 @@ def test_tutorial_query_results_copy_reset_and_error(page, frontend_url):
36
36
  def reply(route):
37
37
  sent.append(route.request.post_data_json)
38
38
  route.fulfill(json={'ok': True, 'data': {'result': {'000001.SZ': {'lastPrice': 11.7}}, 'latency_ms': 12.5}})
39
- page.route('**/api/data/full-tick', reply)
39
+ page.route('**/api/python/xtdata', reply)
40
40
  panel.locator('[name="code_list"]').fill('000001.SZ')
41
41
  panel.locator('[type="submit"]').click()
42
42
  expect(panel.locator('[data-test-status]')).to_have_text('请求完成')
@@ -51,7 +51,7 @@ def test_tutorial_query_results_copy_reset_and_error(page, frontend_url):
51
51
  expect(panel.locator('[data-test-output]')).to_have_text('')
52
52
  panel.locator('[data-test-reset]').click()
53
53
  expect(panel.locator('[name="code_list"]')).to_have_value('000001.SZ,600000.SH')
54
- page.route('**/api/data/full-tick', lambda route: route.fulfill(status=401, json={'ok': False, 'error': 'invalid credentials'}))
54
+ page.route('**/api/python/xtdata', lambda route: route.fulfill(status=401, json={'ok': False, 'error': 'invalid credentials'}))
55
55
  panel.locator('[type="submit"]').click()
56
56
  expect(panel.locator('[data-test-status]')).to_have_text('请求失败')
57
57
  expect(panel.locator('[data-test-output]')).to_contain_text('invalid credentials')
@@ -72,7 +72,7 @@ def test_inline_api_tester_prefers_api_key_when_web_auth_enabled(page, frontend_
72
72
  sent_headers.append(route.request.headers)
73
73
  route.fulfill(json={'ok': True, 'data': {'result': {'000001.SZ': {'lastPrice': 11.7}}}})
74
74
 
75
- page.route('**/api/data/full-tick', reply)
75
+ page.route('**/api/python/xtdata', reply)
76
76
  panel.locator('[name="code_list"]').fill('000001.SZ')
77
77
  panel.locator('[type="submit"]').click()
78
78
  expect(panel.locator('[data-test-output]')).to_contain_text('lastPrice')
@@ -179,8 +179,8 @@ def test_stop_waiting_does_not_resubmit_and_result_survives_navigation(page, fro
179
179
  pending[0].abort()
180
180
  page.evaluate("window.CfquantPythonReference.open('xtdata.get_full_tick')")
181
181
  page.evaluate("window.CfquantPythonReference.open('cftrader.order_stock_batch')")
182
- page.locator('[data-python-test]').click()
183
- expect(page.locator('[data-test-output]')).to_contain_text('避免重复提交')
182
+ page.locator('#tutorialReader [data-python-test]').click()
183
+ expect(page.locator('#tutorialReader [data-test-output]')).to_contain_text('避免重复提交')
184
184
  assert len(pending) == 1
185
185
  assert errors == []
186
186
 
@@ -197,7 +197,7 @@ def test_callback_view_filters_original_events_and_types_are_not_executable(page
197
197
  expect(panel.locator('[data-test-output]')).to_contain_text('1001')
198
198
  assert 'event_name=trader%3Aon_stock_order' in pending[0]
199
199
  page.evaluate("window.CfquantPythonReference.open('type.XtAsset')")
200
- expect(page.locator('[data-python-test]')).to_have_count(0)
200
+ expect(page.locator('#tutorialReader [data-python-test]')).to_have_count(0)
201
201
  assert errors == []
202
202
 
203
203
 
@@ -207,6 +207,9 @@ def test_main_api_batch_testing_summary_and_clear(page, frontend_url):
207
207
  page.mouse.move(page.viewport_size['width'] - 1, page.viewport_size['height'] - 1)
208
208
  page.keyboard.press('Escape')
209
209
  page.locator('.nav-item[data-view="api"]').click()
210
+ group = page.locator('.api-group').filter(has=page.locator('[data-endpoint-id="cftrader.order_stock_batch"]'))
211
+ if group.locator('.api-group-head').get_attribute('aria-expanded') != 'true':
212
+ group.locator('.api-group-head').click()
210
213
  page.locator('[data-endpoint-id="cftrader.order_stock_batch"]').click()
211
214
  sent = []
212
215
 
@@ -234,3 +237,95 @@ def test_main_api_batch_testing_summary_and_clear(page, frontend_url):
234
237
  expect(page.locator('#apiResponseBox')).to_have_text('')
235
238
  expect(page.locator('#apiResultSummary')).to_have_text('')
236
239
  assert errors == []
240
+
241
+
242
+ def test_main_market_timing_success_timeout_and_old_bridge(page, frontend_url):
243
+ requests, errors = open_app(page, frontend_url, setup_required=False)
244
+ page.locator('#closeOnboardingBtn').click()
245
+ page.mouse.move(page.viewport_size['width'] - 1, page.viewport_size['height'] - 1)
246
+ page.keyboard.press('Escape')
247
+ page.locator('.nav-item[data-view="api"]').click()
248
+ page.locator('[data-api-group="data"]').click()
249
+ page.locator('[data-endpoint-id="full_tick"]').click()
250
+ replies = [{
251
+ 'ok': True, 'data': {'latency_ms': 500, 'result': {'000001.SZ': {'lastPrice': 11.3}},
252
+ 'timing': {'qmt': {'available': True, 'queue_ms': 400, 'native_ms': 70,
253
+ 'convert_ms': 5, 'encode_ms': 4, 'fallback_used': True},
254
+ 'rpc': {'wait_ms': 495, 'decode_ms': 2}, 'web': {'result_convert_ms': 3}}}},
255
+ {'ok': False, 'error': 'cfquant pipe request timeout', 'status': 400,
256
+ 'timing': {'qmt': {'available': False, 'reason': 'no_response'}, 'rpc': {'wait_ms': 12000}}},
257
+ {'ok': True, 'data': {'latency_ms': 80, 'result': {},
258
+ 'timing': {'qmt': {'available': False, 'reason': 'bridge_timing_unavailable'}}}},
259
+ ]
260
+ page.route('**/api/data/full-tick', lambda route: route.fulfill(json=replies.pop(0)))
261
+ form = page.locator('#apiForm')
262
+ form.locator('[type="submit"]').click()
263
+ timing = page.locator('#apiResponseLatencyMeta')
264
+ expect(timing).to_contain_text('QMT 原生取数 70.0 ms')
265
+ expect(timing).to_contain_text('兼容格式转换 5.00 ms')
266
+ expect(timing).to_contain_text('Web 结果转换 3.00 ms')
267
+ expect(timing).to_contain_text('旧行情接口已回退到扩展接口')
268
+ assert page.locator('.api-workbench').evaluate('node => node.scrollWidth <= node.clientWidth')
269
+ page.locator('#apiResponseLatency').scroll_into_view_if_needed()
270
+ screenshot(page, 'market-timing')
271
+ form.locator('[type="submit"]').click()
272
+ expect(timing).to_contain_text('QMT 耗时未取得(未收到回包)')
273
+ expect(timing).not_to_contain_text('QMT 原生取数')
274
+ form.locator('[type="submit"]').click()
275
+ expect(timing).to_contain_text('QMT 耗时未取得(桥接脚本未提供)')
276
+ assert errors == []
277
+
278
+
279
+ def test_market_ex_switch_keeps_query_and_displays_response_timing(page, frontend_url):
280
+ requests, errors = open_app(page, frontend_url, setup_required=False)
281
+ page.locator('#closeOnboardingBtn').click()
282
+ page.mouse.move(page.viewport_size['width'] - 1, page.viewport_size['height'] - 1)
283
+ page.keyboard.press('Escape')
284
+ page.locator('.nav-item[data-view="api"]').click()
285
+ page.locator('[data-api-group="data"]').click()
286
+ entry = page.locator('[data-endpoint-id="market_data_ex"]')
287
+ expect(entry).to_contain_text('get_market_data_ex')
288
+ page.locator('[data-endpoint-id="market_data"]').click()
289
+ form = page.locator('#apiForm')
290
+ fields = {'field_list': 'open,close,volume', 'stock_list': '000001.SZ,600000.SH',
291
+ 'start_time': '20260101', 'end_time': '20261014', 'count': '20', 'timeout': '15'}
292
+ for name, value in fields.items():
293
+ form.locator('[name="%s"]' % name).fill(value)
294
+ form.locator('[name="period"]').fill('1d')
295
+ form.locator('[name="fill_data"]').fill('0')
296
+ page.locator('#apiResponseBox').evaluate("node => node.textContent = 'old response'")
297
+ before = page.evaluate('currentApiRequest().body')
298
+ form.locator('[data-market-variant="market_data_ex"]').click()
299
+ expect(page.locator('#apiTitle')).to_contain_text('get_market_data_ex')
300
+ expect(page.locator('#apiRoute')).to_have_text('POST /api/data/market-ex')
301
+ expect(page.locator('#apiResponseBox')).to_have_text('')
302
+ assert page.evaluate('currentApiRequest().body') == before
303
+ pending = []
304
+ page.route('**/api/data/market-ex', lambda route: pending.append(route))
305
+ form.locator('[type="submit"]').click()
306
+ expect(form.locator('[type="submit"]')).to_be_disabled()
307
+ expect(form.locator('[data-market-variant]')).to_be_disabled()
308
+ page.wait_for_function("state.apiDebugBusy")
309
+ assert len(pending) == 1
310
+ assert pending[0].request.post_data_json == before
311
+ assert before['stock_list'] == ['000001.SZ', '600000.SH']
312
+ assert before['field_list'] == ['open', 'close', 'volume']
313
+ assert before['count'] == 20 and before['fill_data'] is False
314
+ pending[0].fulfill(json={'ok': True, 'data': {'latency_ms': 85, 'result': {
315
+ '000001.SZ': {'20261014': {'open': 11.0, 'close': 11.5, 'volume': 120000}}},
316
+ 'timing': {'qmt': {'available': True, 'queue_ms': 20, 'native_ms': 5,
317
+ 'convert_ms': 0, 'encode_ms': 3, 'fallback_used': False},
318
+ 'rpc': {'wait_ms': 80, 'decode_ms': 2}, 'web': {'result_convert_ms': 1}}}})
319
+ expect(page.locator('#apiResponseBox')).to_contain_text('120000')
320
+ expect(page.locator('#apiResponseLatencyMeta')).to_contain_text('QMT 原生取数 5.00 ms')
321
+ expect(page.locator('#apiResponseLatencyMeta')).to_contain_text('QMT 结果编码 3.00 ms')
322
+ expect(page.locator('#apiResponseLatencyMeta')).not_to_contain_text('旧行情接口已回退')
323
+ expect(form.locator('[data-market-variant]')).to_be_enabled()
324
+ assert page.locator('.api-workbench').evaluate('node => node.scrollWidth <= node.clientWidth')
325
+ assert page.locator('#apiEndpointList').evaluate('node => node.scrollWidth <= node.clientWidth')
326
+ screenshot(page, 'market-ex')
327
+ form.locator('[data-market-variant="market_data"]').click()
328
+ expect(page.locator('#apiRoute')).to_have_text('POST /api/data/market')
329
+ assert page.evaluate('currentApiRequest().body') == before
330
+ assert all(method == 'GET' for method, path in requests)
331
+ assert errors == []