cfquant 0.2.50__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.50 → cfquant-0.2.52}/PKG-INFO +1 -1
  2. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/build_info.py +2 -0
  3. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/client.py +9 -5
  4. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/level2.py +7 -1
  5. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/normal_bridge.py +11 -3
  6. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/order_identity.py +17 -0
  7. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/pipe_bridge.py +18 -4
  8. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/pipe_client.py +9 -5
  9. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/protocol.py +65 -18
  10. cfquant-0.2.52/cfquant/qmt_binding_settings.py +39 -0
  11. cfquant-0.2.52/cfquant/qmt_pipe_runtime.py +166 -0
  12. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/qmt_strategy_deploy.py +144 -29
  13. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/qmt_strategy_runtime.py +20 -11
  14. cfquant-0.2.52/cfquant/rpc_timing.py +45 -0
  15. {cfquant-0.2.50 → 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
  16. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_api_tester_browser.py +103 -8
  17. cfquant-0.2.52/cfquant/tests/test_binding_runtime_browser.py +109 -0
  18. cfquant-0.2.52/cfquant/tests/test_dataframe_encoding.py +192 -0
  19. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_lite_deployment_paths.py +18 -9
  20. cfquant-0.2.52/cfquant/tests/test_lite_qmt_dispatch.py +266 -0
  21. cfquant-0.2.52/cfquant/tests/test_market_timing.py +238 -0
  22. cfquant-0.2.52/cfquant/tests/test_qmt_binding_settings.py +123 -0
  23. cfquant-0.2.52/cfquant/tests/test_qmt_fileless.py +282 -0
  24. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_qmt_strategy_deploy.py +150 -16
  25. cfquant-0.2.52/cfquant/tests/test_table_conversion.py +122 -0
  26. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_trade_strategy_callback.py +68 -0
  27. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tx_trade_bridge.py +117 -57
  28. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/version.py +2 -2
  29. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/xttype.py +2 -1
  30. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant.egg-info/PKG-INFO +1 -1
  31. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant.egg-info/SOURCES.txt +10 -0
  32. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant_web_server.py +132 -37
  33. {cfquant-0.2.50 → cfquant-0.2.52}/pyproject.toml +1 -1
  34. {cfquant-0.2.50 → cfquant-0.2.52}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +279 -382
  35. {cfquant-0.2.50 → cfquant-0.2.52}/qmt_scripts/CFQUANT_LITE.py +559 -611
  36. {cfquant-0.2.50 → 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 +228 -371
  37. {cfquant-0.2.50 → 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 +228 -371
  38. {cfquant-0.2.50 → 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 +559 -663
  39. {cfquant-0.2.50 → 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 +559 -663
  40. {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/app.js +104 -6
  41. {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/index.html +24 -2
  42. {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/styles.css +58 -15
  43. {cfquant-0.2.50 → cfquant-0.2.52}/LICENSE +0 -0
  44. {cfquant-0.2.50 → cfquant-0.2.52}/LTtx/__init__.py +0 -0
  45. {cfquant-0.2.50 → cfquant-0.2.52}/LTtx/tx/Config.txt +0 -0
  46. {cfquant-0.2.50 → cfquant-0.2.52}/LTtx/tx/LTtx_server.py +0 -0
  47. {cfquant-0.2.50 → cfquant-0.2.52}/LTtx/tx/__init__.py +0 -0
  48. {cfquant-0.2.50 → cfquant-0.2.52}/LTtx/tx/tx.py +0 -0
  49. {cfquant-0.2.50 → cfquant-0.2.52}/MANIFEST.in +0 -0
  50. {cfquant-0.2.50 → cfquant-0.2.52}/README.md +0 -0
  51. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/__init__.py +0 -0
  52. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/_editable_install.py +0 -0
  53. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/account_routing.py +0 -0
  54. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/batch_orders.py +0 -0
  55. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/callback_dispatcher.py +0 -0
  56. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/cftrader.py +0 -0
  57. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/channels.py +0 -0
  58. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/cli.py +0 -0
  59. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/config.py +0 -0
  60. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/hub.py +0 -0
  61. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/log_management.py +0 -0
  62. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/logging_i18n.py +0 -0
  63. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/management.py +0 -0
  64. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/order_meta.py +0 -0
  65. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/pipe_hub.py +0 -0
  66. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/pipe_transport.py +0 -0
  67. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/qmt_bridge.py +0 -0
  68. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/qmt_strategy_package.py +0 -0
  69. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/runtime_report.py +0 -0
  70. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/stock_connect.py +0 -0
  71. {cfquant-0.2.50 → 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
  72. {cfquant-0.2.50 → 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
  73. {cfquant-0.2.50 → 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
  74. {cfquant-0.2.50 → 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
  75. {cfquant-0.2.50 → 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
  76. {cfquant-0.2.50 → 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
  77. {cfquant-0.2.50 → 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
  78. {cfquant-0.2.50 → 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
  79. {cfquant-0.2.50 → 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
  80. {cfquant-0.2.50 → 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
  81. {cfquant-0.2.50 → 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
  82. {cfquant-0.2.50 → 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
  83. {cfquant-0.2.50 → 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
  84. {cfquant-0.2.50 → 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
  85. {cfquant-0.2.50 → 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
  86. {cfquant-0.2.50 → 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
  87. {cfquant-0.2.50 → 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
  88. {cfquant-0.2.50 → 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
  89. {cfquant-0.2.50 → 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
  90. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/29_Level2/350/241/214/346/203/205/346/265/213/350/257/225.py" +0 -0
  91. {cfquant-0.2.50 → 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
  92. {cfquant-0.2.50 → 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
  93. {cfquant-0.2.50 → 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
  94. {cfquant-0.2.50 → 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
  95. {cfquant-0.2.50 → 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
  96. {cfquant-0.2.50 → 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
  97. {cfquant-0.2.50 → 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
  98. {cfquant-0.2.50 → 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
  99. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/6_/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
  100. {cfquant-0.2.50 → 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
  101. {cfquant-0.2.50 → 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
  102. {cfquant-0.2.50 → 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
  103. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/_helpers.py +0 -0
  104. {cfquant-0.2.50 → 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
  105. {cfquant-0.2.50 → 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
  106. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/readme.md +0 -0
  107. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_advanced_parity.py +0 -0
  108. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_api_tester_web.py +0 -0
  109. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
  110. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_callback_requests.py +0 -0
  111. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_cftrader.py +0 -0
  112. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_cftrader_tutorial.py +0 -0
  113. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_cftrader_web.py +0 -0
  114. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_credit_compat.py +0 -0
  115. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_json_serialization.py +0 -0
  116. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_lite_account_routing.py +0 -0
  117. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_lite_parity.py +0 -0
  118. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_local_trading_calendar.py +0 -0
  119. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_log_management.py +0 -0
  120. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_management.py +0 -0
  121. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_management_batch.py +0 -0
  122. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_market_data_fallback.py +0 -0
  123. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_order_identity.py +0 -0
  124. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_order_time_callback.py +0 -0
  125. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_project_updater.py +0 -0
  126. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_python_api_reference.py +0 -0
  127. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_qmt_strategy_browser.py +0 -0
  128. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_qmt_strategy_web.py +0 -0
  129. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
  130. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_startup_recovery.py +0 -0
  131. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_stock_connect.py +0 -0
  132. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_stock_connect_browser.py +0 -0
  133. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_trading_dates.py +0 -0
  134. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_tutorial_reader.py +0 -0
  135. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tests/test_tx_auto_import.py +0 -0
  136. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/trading_calendar.py +0 -0
  137. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/tx.py +0 -0
  138. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/xtconstant.py +0 -0
  139. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/xtdata.py +0 -0
  140. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant/xttrader.py +0 -0
  141. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant.egg-info/dependency_links.txt +0 -0
  142. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant.egg-info/entry_points.txt +0 -0
  143. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant.egg-info/requires.txt +0 -0
  144. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant.egg-info/top_level.txt +0 -0
  145. {cfquant-0.2.50 → cfquant-0.2.52}/cfquant_pipe_hub.py +0 -0
  146. {cfquant-0.2.50 → cfquant-0.2.52}/qmt_scripts/CFQUANT.py +0 -0
  147. {cfquant-0.2.50 → cfquant-0.2.52}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
  148. {cfquant-0.2.50 → cfquant-0.2.52}/qmt_scripts/__init__.py +0 -0
  149. {cfquant-0.2.50 → 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.50 → 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.50 → 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.50 → cfquant-0.2.52}/requirements.txt +0 -0
  153. {cfquant-0.2.50 → cfquant-0.2.52}/setup.cfg +0 -0
  154. {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/__init__.py +0 -0
  155. {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/api-tester.js +0 -0
  156. {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/avatars/amber-pulse.svg +0 -0
  157. {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/avatars/copper-grid.svg +0 -0
  158. {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/avatars/market-blue.svg +0 -0
  159. {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/avatars/rose-circuit.svg +0 -0
  160. {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/avatars/signal-green.svg +0 -0
  161. {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/avatars/slate-wave.svg +0 -0
  162. {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/avatars/teal-orbit.svg +0 -0
  163. {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/avatars/violet-node.svg +0 -0
  164. {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/cftrader-api-data.js +0 -0
  165. {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/docs.js +0 -0
  166. {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/favicon.svg +0 -0
  167. {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
  168. {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
  169. {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
  170. {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
  171. {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
  172. {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
  173. {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
  174. {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
  175. {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
  176. {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
  177. {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
  178. {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
  179. {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
  180. {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
  181. {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
  182. {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
  183. {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
  184. {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
  185. {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/python-api-data.js +0 -0
  186. {cfquant-0.2.50 → cfquant-0.2.52}/web_dashboard/python-api.js +0 -0
  187. {cfquant-0.2.50 → 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.50
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
@@ -29,6 +29,8 @@ def get_git_commit(source_root=None):
29
29
  override = _valid_commit(os.environ.get("CFQUANT_GIT_COMMIT"))
30
30
  if override:
31
31
  return override
32
+ if os.environ.get("CFQUANT_QMT_NO_FILE_IO") == "1":
33
+ return ""
32
34
  root = Path(source_root or Path(__file__).resolve().parent.parent).resolve()
33
35
  try:
34
36
  result = subprocess.run(["git", "-C", str(root), "rev-parse", "HEAD"], stdout=subprocess.PIPE,
@@ -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:
@@ -10,7 +10,7 @@ import threading
10
10
  import time
11
11
 
12
12
  from . import order_meta
13
- from .order_identity import prepare_order_remark, order_remark_key, restore_order_remark
13
+ from .order_identity import prepare_order_remark, order_remark_key, restore_order_remark, restore_strategy_name
14
14
  from .level2 import L2_THOUSAND_SUBSCRIPTIONS, quote_callback_data, quote_plain, require_l2_callable, thousand_price
15
15
  from .protocol import loads_message, pack_event, pack_response
16
16
  from .tx_trade_bridge import TxTradeBridge, relay_sync_order_callback
@@ -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})
@@ -191,6 +192,12 @@ class NormalQmtBridge(TxTradeBridge):
191
192
  if action == "cfquant.status":
192
193
  self._send_response(msg, self._status())
193
194
  return
195
+ if action == "cfquant.strategy.control":
196
+ try:
197
+ self._send_response(msg, self._dispatch(action, msg.get("params") or {}, msg))
198
+ except Exception as error:
199
+ self._send_error(msg, error)
200
+ return
194
201
  if self._try_enqueue_coalesced_request(msg):
195
202
  return
196
203
  try:
@@ -834,7 +841,7 @@ class NormalQmtBridge(TxTradeBridge):
834
841
  client_id = msg.get("client_id") or msg.get("reply_channel")
835
842
  if not client_id:
836
843
  return
837
- 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"))
838
845
  self.tx.push("response", response, client_id)
839
846
 
840
847
  def _send_error(self, msg, error):
@@ -845,7 +852,7 @@ class NormalQmtBridge(TxTradeBridge):
845
852
  "normal bridge send_error action=%s id=%s client_id=%s error=%s"
846
853
  % (msg.get("action"), msg.get("id"), client_id, error)
847
854
  )
848
- 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"))
849
856
  self.tx.push("response", response, client_id)
850
857
 
851
858
  def publish_callback_event(self, event_name, obj):
@@ -894,6 +901,7 @@ class NormalQmtBridge(TxTradeBridge):
894
901
  relay_sync_order_callback(self.context, data)
895
902
  self._handle_async_order_callback(data)
896
903
  self._remember_order_callback_fields(data)
904
+ restore_strategy_name(data)
897
905
  payload = {
898
906
  "type": "event",
899
907
  "event": event_name,
@@ -12,6 +12,23 @@ from collections import OrderedDict
12
12
 
13
13
 
14
14
  _ORDER_REMARK_SUFFIX = re.compile(r"__cfq_([0-9a-f]{32})$")
15
+ _STRATEGY_NAME_SUFFIX = re.compile(r"(?:&&&_cfq_|^cfq_)[0-9a-f]{12}\Z")
16
+
17
+
18
+ def restore_strategy_name(data):
19
+ """Remove our request token only at the public callback/object boundary.
20
+
21
+ Keep the full strategy until error-context matching has finished. Match
22
+ the generated suffix precisely so user underscores and delimiters survive.
23
+ """
24
+ if isinstance(data, dict):
25
+ for name in ("strategy_name", "m_strStrategyName", "mstrStrategyName", "strategyName"):
26
+ value = data.get(name)
27
+ if isinstance(value, str):
28
+ match = _STRATEGY_NAME_SUFFIX.search(value)
29
+ if match:
30
+ data[name] = value[:match.start()]
31
+ return data
15
32
 
16
33
 
17
34
  def original_order_remark(value):
@@ -8,7 +8,9 @@ from .pipe_transport import DEFAULT_PIPE_NAME, PipeTxClient
8
8
  from .tx_trade_bridge import TxTradeBridge
9
9
 
10
10
 
11
- class PipeNormalQmtBridge(NormalQmtBridge):
11
+ class PipeNormalQmtBridge(NormalQmtBridge):
12
+ file_io_enabled = False
13
+
12
14
  def __init__(
13
15
  self,
14
16
  context,
@@ -48,6 +50,9 @@ class PipeNormalQmtBridge(NormalQmtBridge):
48
50
  self.connect_timeout_ms = int(connect_timeout_ms)
49
51
 
50
52
  def start(self):
53
+ runtime = self.globals_dict.get("_managed_runtime")
54
+ if runtime is not None:
55
+ runtime.attach_bridge(self)
51
56
  if self.running:
52
57
  return self
53
58
  self.running = True
@@ -61,7 +66,9 @@ class PipeNormalQmtBridge(NormalQmtBridge):
61
66
  endpoint_name="normal",
62
67
  show=self.show,
63
68
  connect_timeout_ms=self.connect_timeout_ms,
64
- ).start()
69
+ )
70
+ self.tx._log = self._log
71
+ self.tx.start()
65
72
  self.tx.start_txg(self.request_channel)
66
73
  self.recv_thread = threading.Thread(target=self._recv_loop)
67
74
  self.recv_thread.daemon = True
@@ -103,7 +110,9 @@ class PipeNormalQmtBridge(NormalQmtBridge):
103
110
  return data
104
111
 
105
112
 
106
- class PipeTradeBridge(TxTradeBridge):
113
+ class PipeTradeBridge(TxTradeBridge):
114
+ file_io_enabled = False
115
+
107
116
  def __init__(
108
117
  self,
109
118
  context,
@@ -131,6 +140,9 @@ class PipeTradeBridge(TxTradeBridge):
131
140
  self.connect_timeout_ms = int(connect_timeout_ms)
132
141
 
133
142
  def start(self):
143
+ runtime = self.globals_dict.get("_managed_runtime")
144
+ if runtime is not None:
145
+ runtime.attach_bridge(self)
134
146
  if self.running:
135
147
  return self
136
148
  self.running = True
@@ -143,7 +155,9 @@ class PipeTradeBridge(TxTradeBridge):
143
155
  endpoint_name="trade",
144
156
  show=self.show,
145
157
  connect_timeout_ms=self.connect_timeout_ms,
146
- ).start()
158
+ )
159
+ self.tx._log = self._log
160
+ self.tx.start()
147
161
  self.tx.start_txg(self.request_channel)
148
162
  self._log(
149
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,166 @@
1
+ """In-memory managed strategy control over the existing Pipe connection (QMT 3.6+)."""
2
+
3
+ import functools as _cq_functools
4
+ import os as _cq_os
5
+ import sys as _cq_sys
6
+ import threading as _cq_threading
7
+ import time as _cq_time
8
+
9
+
10
+ class ManagedPipeRuntime(object):
11
+ def __init__(self, descriptor=None):
12
+ self.descriptor = dict(descriptor or {})
13
+ self.active = False
14
+ self.approved = False
15
+ self.revoked = False
16
+ self.namespace = {}
17
+ self.original_stop = None
18
+ self.failure = ""
19
+ self.calls = 0
20
+ self.bridges_closed = False
21
+ self.attached_bridges = []
22
+ self.mutex = _cq_threading.RLock()
23
+
24
+ def acquire(self):
25
+ if not self.descriptor:
26
+ self.active = True
27
+ return self
28
+ # sys survives strategy namespace reloads inside one QMT process.
29
+ # Across processes the Pipe hub arbitrates ownership of request channels.
30
+ registry = _cq_sys.__dict__.setdefault("_cfquant_pipe_strategies", {})
31
+ lock = _cq_sys.__dict__.setdefault("_cfquant_pipe_strategies_lock", _cq_threading.RLock())
32
+ with lock:
33
+ key = (self.descriptor["runtime_key"], self.descriptor["role"])
34
+ incompatible = any(scope[0] == key[0] and (
35
+ owner.descriptor.get("mode") != self.descriptor["mode"]
36
+ or owner.descriptor.get("generation") != self.descriptor["generation"])
37
+ for scope, owner in registry.items())
38
+ if key in registry or incompatible:
39
+ raise RuntimeError("Another cfquant strategy still owns this QMT account role.")
40
+ registry[key] = self
41
+ self.active = True
42
+ return self
43
+
44
+ def status(self):
45
+ bridges = [self.namespace.get(name) for name in ("_normal_bridge", "_trade_bridge")
46
+ if self.namespace.get(name) is not None]
47
+ ready = bool(bridges) and all(getattr(bridge, "running", False)
48
+ and getattr(bridge, "context", None) is not None for bridge in bridges)
49
+ state = "error" if self.failure else ("stopped" if not self.active or self.revoked
50
+ else ("running" if ready and self.approved else "loaded"))
51
+ return dict(self.descriptor, state=state, error=self.failure,
52
+ pid=_cq_os.getpid(), updated_at=_cq_time.time())
53
+
54
+ def control(self, desired):
55
+ if desired.get("runtime_key") != self.descriptor.get("runtime_key"):
56
+ raise ValueError("Managed strategy account scope mismatch")
57
+ accepted = desired.get("accepted_generations") or []
58
+ if not isinstance(accepted, (tuple, list)):
59
+ accepted = []
60
+ allowed = (desired.get("enabled") is True
61
+ and desired.get("mode") == self.descriptor["mode"]
62
+ and self.descriptor["generation"] in [desired.get("generation")] + accepted)
63
+ with self.mutex:
64
+ self.approved = allowed and self.active and not self.revoked
65
+ if not allowed and not self.revoked:
66
+ self.revoked = True
67
+ # Block requests immediately; let the control response leave before closing Pipe.
68
+ worker = _cq_threading.Timer(0.1, self.close)
69
+ worker.daemon = True
70
+ worker.start()
71
+ return self.status()
72
+
73
+ def dispatch(self, function):
74
+ dispatch_request = self.wrap(function, reject=True)
75
+ @_cq_functools.wraps(function)
76
+ def guarded(action, params, msg):
77
+ if action == "cfquant.strategy.control":
78
+ return self.control(params or {})
79
+ if action == "cfquant.status":
80
+ return function(action, params, msg)
81
+ if not self.approved:
82
+ raise RuntimeError("Managed strategy is awaiting account mode confirmation from cfquant Web.")
83
+ return dispatch_request(action, params, msg)
84
+ return guarded
85
+
86
+ def wrap(self, function, reject=False):
87
+ @_cq_functools.wraps(function)
88
+ def guarded(*args, **kwargs):
89
+ with self.mutex:
90
+ if not self.active or self.revoked:
91
+ if reject:
92
+ raise RuntimeError("The account's cfquant strategy mode has changed.")
93
+ return None
94
+ self.calls += 1
95
+ try:
96
+ return function(*args, **kwargs)
97
+ except Exception as error:
98
+ if function.__name__ == "init":
99
+ self.failure = str(error)
100
+ self.close()
101
+ raise
102
+ finally:
103
+ with self.mutex:
104
+ self.calls -= 1
105
+ self.release_if_idle()
106
+ return guarded
107
+
108
+ def bind(self, namespace):
109
+ if not self.descriptor:
110
+ return
111
+ self.namespace = namespace
112
+ self.original_stop = namespace.get("stop")
113
+ for name, value in list(namespace.items()):
114
+ if (name in ("init", "after_init", "handlebar", "cfquant_normal_timer", "cfquant_ctype_trade_timer")
115
+ or name.endswith("_callback")) and callable(value):
116
+ namespace[name] = self.wrap(value)
117
+ for name in ("_normal_bridge", "_trade_bridge"):
118
+ bridge = namespace.get(name)
119
+ if bridge is not None:
120
+ self.attach_bridge(bridge)
121
+ namespace["stop"] = self.close
122
+
123
+ def attach_bridge(self, bridge):
124
+ if bridge not in self.attached_bridges:
125
+ self.attached_bridges.append(bridge)
126
+ if not self.descriptor or getattr(bridge, "managed_runtime", None) is self:
127
+ return
128
+ bridge.managed_runtime = self
129
+ bridge._dispatch = self.dispatch(bridge._dispatch)
130
+ for method in ("start", "poll", "pump"):
131
+ function = getattr(bridge, method, None)
132
+ if callable(function):
133
+ setattr(bridge, method, self.wrap(function))
134
+
135
+ def release_if_idle(self):
136
+ if self.bridges_closed and self.calls == 0:
137
+ registry = _cq_sys.__dict__.get("_cfquant_pipe_strategies", {})
138
+ key = (self.descriptor.get("runtime_key"), self.descriptor.get("role"))
139
+ lock = _cq_sys.__dict__.setdefault("_cfquant_pipe_strategies_lock", _cq_threading.RLock())
140
+ with lock:
141
+ if registry.get(key) is self:
142
+ registry.pop(key, None)
143
+
144
+ def close(self, context=None):
145
+ with self.mutex:
146
+ if not self.active:
147
+ return
148
+ self.active = False
149
+ self.approved = False
150
+ try:
151
+ if self.original_stop is not None:
152
+ self.original_stop(context)
153
+ else:
154
+ bridges = list(self.attached_bridges)
155
+ for name in ("_normal_bridge", "_trade_bridge"):
156
+ bridge = self.namespace.get(name)
157
+ if bridge is not None and bridge not in bridges:
158
+ bridges.append(bridge)
159
+ for bridge in bridges:
160
+ bridge.close()
161
+ with self.mutex:
162
+ self.bridges_closed = True
163
+ self.release_if_idle()
164
+ except Exception as error:
165
+ self.failure = str(error)
166
+ print("cfquant strategy shutdown failed: %s" % error)