cfquant 0.2.38__tar.gz → 0.2.40__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 (158) hide show
  1. {cfquant-0.2.38 → cfquant-0.2.40}/PKG-INFO +1 -1
  2. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/__init__.py +14 -3
  3. cfquant-0.2.40/cfquant/build_info.py +65 -0
  4. cfquant-0.2.40/cfquant/callback_dispatcher.py +94 -0
  5. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/client.py +57 -68
  6. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/pipe_client.py +63 -76
  7. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/qmt_bridge.py +18 -13
  8. cfquant-0.2.40/cfquant/tests/test_callback_requests.py +256 -0
  9. cfquant-0.2.40/cfquant/tests/test_order_time_callback.py +71 -0
  10. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/tx_trade_bridge.py +17 -12
  11. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/version.py +2 -2
  12. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/xttrader.py +12 -3
  13. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/xttype.py +43 -4
  14. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant.egg-info/PKG-INFO +1 -1
  15. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant.egg-info/SOURCES.txt +4 -0
  16. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant_web_server.py +30 -14
  17. {cfquant-0.2.38 → cfquant-0.2.40}/pyproject.toml +1 -1
  18. {cfquant-0.2.38 → cfquant-0.2.40}/qmt_scripts/CFQUANT_LITE.py +27 -3
  19. {cfquant-0.2.38 → cfquant-0.2.40}/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 +27 -3
  20. {cfquant-0.2.38 → cfquant-0.2.40}/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 +27 -3
  21. {cfquant-0.2.38 → cfquant-0.2.40}/web_dashboard/app.js +142 -22
  22. {cfquant-0.2.38 → cfquant-0.2.40}/web_dashboard/index.html +12 -3
  23. {cfquant-0.2.38 → cfquant-0.2.40}/web_dashboard/styles.css +89 -33
  24. {cfquant-0.2.38 → cfquant-0.2.40}/LICENSE +0 -0
  25. {cfquant-0.2.38 → cfquant-0.2.40}/LTtx/__init__.py +0 -0
  26. {cfquant-0.2.38 → cfquant-0.2.40}/LTtx/tx/Config.txt +0 -0
  27. {cfquant-0.2.38 → cfquant-0.2.40}/LTtx/tx/LTtx_server.py +0 -0
  28. {cfquant-0.2.38 → cfquant-0.2.40}/LTtx/tx/__init__.py +0 -0
  29. {cfquant-0.2.38 → cfquant-0.2.40}/LTtx/tx/tx.py +0 -0
  30. {cfquant-0.2.38 → cfquant-0.2.40}/MANIFEST.in +0 -0
  31. {cfquant-0.2.38 → cfquant-0.2.40}/README.md +0 -0
  32. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/_editable_install.py +0 -0
  33. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/account_routing.py +0 -0
  34. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/batch_orders.py +0 -0
  35. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/cftrader.py +0 -0
  36. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/channels.py +0 -0
  37. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/cli.py +0 -0
  38. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/config.py +0 -0
  39. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/hub.py +0 -0
  40. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/level2.py +0 -0
  41. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/log_management.py +0 -0
  42. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/logging_i18n.py +0 -0
  43. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/normal_bridge.py +0 -0
  44. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/order_meta.py +0 -0
  45. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/pipe_bridge.py +0 -0
  46. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/pipe_hub.py +0 -0
  47. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/pipe_transport.py +0 -0
  48. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/protocol.py +0 -0
  49. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/qmt_strategy_deploy.py +0 -0
  50. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/qmt_strategy_package.py +0 -0
  51. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/qmt_strategy_runtime.py +0 -0
  52. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/runtime_report.py +0 -0
  53. {cfquant-0.2.38 → cfquant-0.2.40}/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
  54. {cfquant-0.2.38 → cfquant-0.2.40}/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
  55. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/tests/12_/350/256/242/345/215/225/345/244/207/346/263/250/346/265/213/350/257/225.py" +0 -0
  56. {cfquant-0.2.38 → cfquant-0.2.40}/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
  57. {cfquant-0.2.38 → cfquant-0.2.40}/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
  58. {cfquant-0.2.38 → cfquant-0.2.40}/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
  59. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/tests/16_/345/220/257/345/212/250/345/256/211/350/243/205/346/265/213/350/257/225.py" +0 -0
  60. {cfquant-0.2.38 → cfquant-0.2.40}/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
  61. {cfquant-0.2.38 → cfquant-0.2.40}/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
  62. {cfquant-0.2.38 → cfquant-0.2.40}/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
  63. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/tests/1_/350/241/214/346/203/205/346/216/245/346/224/266/346/265/213/350/257/225.py" +0 -0
  64. {cfquant-0.2.38 → cfquant-0.2.40}/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
  65. {cfquant-0.2.38 → cfquant-0.2.40}/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
  66. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/tests/22_/350/277/236/346/216/245/350/257/212/346/226/255/346/265/213/350/257/225.py" +0 -0
  67. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/tests/23_/347/244/272/344/276/213/351/205/215/347/275/256/346/265/213/350/257/225.py" +0 -0
  68. {cfquant-0.2.38 → cfquant-0.2.40}/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
  69. {cfquant-0.2.38 → cfquant-0.2.40}/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
  70. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/tests/26_/345/205/250/346/216/250/350/256/242/351/230/205/346/265/213/350/257/225.py" +0 -0
  71. {cfquant-0.2.38 → cfquant-0.2.40}/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
  72. {cfquant-0.2.38 → cfquant-0.2.40}/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
  73. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/tests/29_Level2/350/241/214/346/203/205/346/265/213/350/257/225.py" +0 -0
  74. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/tests/2_/346/225/260/346/215/256/350/216/267/345/217/226/346/265/213/350/257/225.py" +0 -0
  75. {cfquant-0.2.38 → cfquant-0.2.40}/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
  76. {cfquant-0.2.38 → cfquant-0.2.40}/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
  77. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/tests/32_/345/274/202/346/255/245/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  78. {cfquant-0.2.38 → cfquant-0.2.40}/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
  79. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/tests/3_/346/225/260/346/215/256/344/270/213/350/275/275/346/265/213/350/257/225.py" +0 -0
  80. {cfquant-0.2.38 → cfquant-0.2.40}/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
  81. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/tests/5_/347/234/237/345/256/236/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  82. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/tests/6_/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
  83. {cfquant-0.2.38 → cfquant-0.2.40}/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
  84. {cfquant-0.2.38 → cfquant-0.2.40}/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
  85. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/tests/9_/345/217/257/346/222/244/350/256/242/345/215/225/346/265/213/350/257/225.py" +0 -0
  86. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/tests/_helpers.py +0 -0
  87. {cfquant-0.2.38 → cfquant-0.2.40}/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
  88. {cfquant-0.2.38 → cfquant-0.2.40}/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
  89. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/tests/readme.md +0 -0
  90. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/tests/test_api_tester_browser.py +0 -0
  91. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/tests/test_api_tester_web.py +0 -0
  92. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
  93. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/tests/test_cftrader.py +0 -0
  94. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/tests/test_cftrader_tutorial.py +0 -0
  95. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/tests/test_cftrader_web.py +0 -0
  96. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/tests/test_credit_compat.py +0 -0
  97. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/tests/test_json_serialization.py +0 -0
  98. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/tests/test_log_management.py +0 -0
  99. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/tests/test_management_batch.py +0 -0
  100. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/tests/test_project_updater.py +0 -0
  101. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/tests/test_python_api_reference.py +0 -0
  102. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/tests/test_qmt_strategy_browser.py +0 -0
  103. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/tests/test_qmt_strategy_deploy.py +0 -0
  104. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/tests/test_qmt_strategy_web.py +0 -0
  105. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/tests/test_tutorial_reader.py +0 -0
  106. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/tests/test_tx_auto_import.py +0 -0
  107. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/tx.py +0 -0
  108. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/xtconstant.py +0 -0
  109. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant/xtdata.py +0 -0
  110. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant.egg-info/dependency_links.txt +0 -0
  111. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant.egg-info/entry_points.txt +0 -0
  112. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant.egg-info/requires.txt +0 -0
  113. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant.egg-info/top_level.txt +0 -0
  114. {cfquant-0.2.38 → cfquant-0.2.40}/cfquant_pipe_hub.py +0 -0
  115. {cfquant-0.2.38 → cfquant-0.2.40}/qmt_scripts/CFQUANT.py +0 -0
  116. {cfquant-0.2.38 → cfquant-0.2.40}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +0 -0
  117. {cfquant-0.2.38 → cfquant-0.2.40}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
  118. {cfquant-0.2.38 → cfquant-0.2.40}/qmt_scripts/__init__.py +0 -0
  119. {cfquant-0.2.38 → cfquant-0.2.40}/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
  120. {cfquant-0.2.38 → cfquant-0.2.40}/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
  121. {cfquant-0.2.38 → cfquant-0.2.40}/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
  122. {cfquant-0.2.38 → cfquant-0.2.40}/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
  123. {cfquant-0.2.38 → cfquant-0.2.40}/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
  124. {cfquant-0.2.38 → cfquant-0.2.40}/requirements.txt +0 -0
  125. {cfquant-0.2.38 → cfquant-0.2.40}/setup.cfg +0 -0
  126. {cfquant-0.2.38 → cfquant-0.2.40}/web_dashboard/__init__.py +0 -0
  127. {cfquant-0.2.38 → cfquant-0.2.40}/web_dashboard/api-tester.js +0 -0
  128. {cfquant-0.2.38 → cfquant-0.2.40}/web_dashboard/avatars/amber-pulse.svg +0 -0
  129. {cfquant-0.2.38 → cfquant-0.2.40}/web_dashboard/avatars/copper-grid.svg +0 -0
  130. {cfquant-0.2.38 → cfquant-0.2.40}/web_dashboard/avatars/market-blue.svg +0 -0
  131. {cfquant-0.2.38 → cfquant-0.2.40}/web_dashboard/avatars/rose-circuit.svg +0 -0
  132. {cfquant-0.2.38 → cfquant-0.2.40}/web_dashboard/avatars/signal-green.svg +0 -0
  133. {cfquant-0.2.38 → cfquant-0.2.40}/web_dashboard/avatars/slate-wave.svg +0 -0
  134. {cfquant-0.2.38 → cfquant-0.2.40}/web_dashboard/avatars/teal-orbit.svg +0 -0
  135. {cfquant-0.2.38 → cfquant-0.2.40}/web_dashboard/avatars/violet-node.svg +0 -0
  136. {cfquant-0.2.38 → cfquant-0.2.40}/web_dashboard/cftrader-api-data.js +0 -0
  137. {cfquant-0.2.38 → cfquant-0.2.40}/web_dashboard/favicon.svg +0 -0
  138. {cfquant-0.2.38 → cfquant-0.2.40}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
  139. {cfquant-0.2.38 → cfquant-0.2.40}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
  140. {cfquant-0.2.38 → cfquant-0.2.40}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
  141. {cfquant-0.2.38 → cfquant-0.2.40}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
  142. {cfquant-0.2.38 → cfquant-0.2.40}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
  143. {cfquant-0.2.38 → cfquant-0.2.40}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
  144. {cfquant-0.2.38 → cfquant-0.2.40}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
  145. {cfquant-0.2.38 → cfquant-0.2.40}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
  146. {cfquant-0.2.38 → cfquant-0.2.40}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
  147. {cfquant-0.2.38 → cfquant-0.2.40}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
  148. {cfquant-0.2.38 → cfquant-0.2.40}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
  149. {cfquant-0.2.38 → cfquant-0.2.40}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
  150. {cfquant-0.2.38 → cfquant-0.2.40}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
  151. {cfquant-0.2.38 → cfquant-0.2.40}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
  152. {cfquant-0.2.38 → cfquant-0.2.40}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
  153. {cfquant-0.2.38 → cfquant-0.2.40}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
  154. {cfquant-0.2.38 → cfquant-0.2.40}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
  155. {cfquant-0.2.38 → cfquant-0.2.40}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
  156. {cfquant-0.2.38 → cfquant-0.2.40}/web_dashboard/python-api-data.js +0 -0
  157. {cfquant-0.2.38 → cfquant-0.2.40}/web_dashboard/python-api.js +0 -0
  158. {cfquant-0.2.38 → cfquant-0.2.40}/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.38
3
+ Version: 0.2.40
4
4
  Summary: QMT local bridge and xtquant-compatible Python client.
5
5
  License: MIT
6
6
  Requires-Python: >=3.8
@@ -12,7 +12,13 @@ cfquant: 面向 QMT 的 xtquant 兼容层。
12
12
  from . import xtconstant, xtdata, xttrader, xttype
13
13
  from . import cftrader
14
14
  from .client import CfquantError, CfquantTimeout, configure, get_client
15
- from .version import __version__
15
+ from .version import __version__
16
+ from .build_info import build_identity, get_git_commit
17
+
18
+ _BUILD_IDENTITY = build_identity(__version__)
19
+ GIT_COMMIT = _BUILD_IDENTITY["git_commit"]
20
+ SHORT_GIT_COMMIT = _BUILD_IDENTITY["short_commit"]
21
+ BUILD_VERSION = _BUILD_IDENTITY["build_version"]
16
22
 
17
23
  __all__ = [
18
24
  "xtconstant",
@@ -24,5 +30,10 @@ __all__ = [
24
30
  "get_client",
25
31
  "CfquantError",
26
32
  "CfquantTimeout",
27
- "__version__",
28
- ]
33
+ "__version__",
34
+ "GIT_COMMIT",
35
+ "SHORT_GIT_COMMIT",
36
+ "BUILD_VERSION",
37
+ "get_git_commit",
38
+ "build_identity",
39
+ ]
@@ -0,0 +1,65 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Build and source identity helpers."""
3
+ import os
4
+ import re
5
+ import subprocess
6
+ from functools import lru_cache
7
+ from pathlib import Path
8
+
9
+ _COMMIT_RE = re.compile(r"^[0-9a-f]{40}$", re.IGNORECASE)
10
+
11
+ def _valid_commit(value):
12
+ value = str(value or "").strip().lower()
13
+ return value if _COMMIT_RE.fullmatch(value) else ""
14
+
15
+ def _read_git_dir(source_root):
16
+ marker = Path(source_root) / ".git"
17
+ if marker.is_dir():
18
+ return marker
19
+ if marker.is_file():
20
+ text = marker.read_text(encoding="utf-8", errors="replace").strip()
21
+ if text.lower().startswith("gitdir:"):
22
+ path = Path(text.split(":", 1)[1].strip())
23
+ return path if path.is_absolute() else (marker.parent / path).resolve()
24
+ return None
25
+
26
+ @lru_cache(maxsize=8)
27
+ def get_git_commit(source_root=None):
28
+ """Return the full source commit, or an empty string when unavailable."""
29
+ override = _valid_commit(os.environ.get("CFQUANT_GIT_COMMIT"))
30
+ if override:
31
+ return override
32
+ root = Path(source_root or Path(__file__).resolve().parent.parent).resolve()
33
+ try:
34
+ result = subprocess.run(["git", "-C", str(root), "rev-parse", "HEAD"], stdout=subprocess.PIPE,
35
+ stderr=subprocess.DEVNULL, text=True, timeout=2, check=False)
36
+ commit = _valid_commit(result.stdout)
37
+ if commit:
38
+ return commit
39
+ except Exception:
40
+ pass
41
+ git_dir = _read_git_dir(root)
42
+ if not git_dir:
43
+ return ""
44
+ try:
45
+ head = (git_dir / "HEAD").read_text(encoding="ascii", errors="ignore").strip()
46
+ if head.startswith("ref: "):
47
+ ref = head[5:].strip()
48
+ ref_path = git_dir / ref
49
+ if ref_path.is_file():
50
+ return _valid_commit(ref_path.read_text(encoding="ascii", errors="ignore"))
51
+ packed = git_dir / "packed-refs"
52
+ if packed.is_file():
53
+ for line in packed.read_text(encoding="ascii", errors="ignore").splitlines():
54
+ parts = line.split()
55
+ if len(parts) == 2 and parts[1] == ref:
56
+ return _valid_commit(parts[0])
57
+ return _valid_commit(head)
58
+ except Exception:
59
+ return ""
60
+
61
+ def build_identity(version, source_root=None):
62
+ commit = get_git_commit(source_root)
63
+ short = commit[:7] if commit else ""
64
+ return {"git_commit": commit, "short_commit": short,
65
+ "build_version": "%s+g%s" % (version, short) if version and short else str(version or "")}
@@ -0,0 +1,94 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Serial user callbacks, independent of the RPC receive thread."""
3
+ from collections import deque
4
+ import logging
5
+ import threading
6
+
7
+ from .protocol import decode_value
8
+
9
+
10
+ class CallbackDispatcher(object):
11
+ def __init__(self, dispatch):
12
+ self._dispatch = dispatch
13
+ self._condition = threading.Condition()
14
+ self._queue = deque()
15
+ self._generation = 0
16
+ self._active = False
17
+ self._thread = None
18
+ self._context = threading.local()
19
+
20
+ def start(self):
21
+ with self._condition:
22
+ self._generation += 1
23
+ self._active = True
24
+ self._queue.clear()
25
+ return self._generation
26
+
27
+ def stop(self):
28
+ with self._condition:
29
+ self._active = False
30
+ self._generation += 1
31
+ self._queue.clear()
32
+ self._condition.notify_all()
33
+ # Never join: close/stop may be called by a callback itself.
34
+
35
+ def submit(self, generation, message):
36
+ with self._condition:
37
+ if not self._active or generation != self._generation:
38
+ return
39
+ # Do not block the receive thread on a slow user callback. Trade
40
+ # events cannot be silently dropped or coalesced.
41
+ self._queue.append((generation, message))
42
+ if self._thread is None:
43
+ self._thread = threading.Thread(target=self._run, name="cfquant-event-callback")
44
+ self._thread.daemon = True
45
+ self._thread.start()
46
+ self._condition.notify()
47
+
48
+ def is_current_callback(self):
49
+ generation = getattr(self._context, "generation", None)
50
+ with self._condition:
51
+ return generation is None or (self._active and generation == self._generation)
52
+
53
+ def check_request(self):
54
+ if not self.is_current_callback():
55
+ raise RuntimeError("callback belongs to a closed cfquant connection")
56
+
57
+ def _run(self):
58
+ while True:
59
+ with self._condition:
60
+ while self._active and not self._queue:
61
+ self._condition.wait()
62
+ if not self._active:
63
+ self._thread = None
64
+ return
65
+ generation, message = self._queue.popleft()
66
+ self._context.generation = generation
67
+ try:
68
+ if self.is_current_callback():
69
+ self._dispatch(message)
70
+ except Exception:
71
+ logging.getLogger(__name__).exception("callback dispatch failed event=%s", message.get("event"))
72
+ finally:
73
+ del self._context.generation
74
+
75
+
76
+ def dispatch_callbacks(callbacks, message, is_current):
77
+ event = message.get("event")
78
+ data = decode_value(message.get("data"))
79
+ full_message = dict(message, data=data)
80
+ deliveries = [(list(callbacks.get("__event__", [])), full_message),
81
+ (list(callbacks.get(event, [])), data)]
82
+ if event and event.startswith("quote:"):
83
+ quote_message = dict(full_message)
84
+ if quote_message.get("subscription_id") is not None and quote_message.get("subscribe_id") is None:
85
+ quote_message["subscribe_id"] = quote_message["subscription_id"]
86
+ deliveries.append((list(callbacks.get("quote", [])), quote_message))
87
+ for handlers, payload in deliveries:
88
+ for callback in handlers:
89
+ if not is_current():
90
+ return
91
+ try:
92
+ callback(payload)
93
+ except Exception:
94
+ logging.getLogger(__name__).exception("user callback failed event=%s", event)
@@ -6,6 +6,7 @@ import threading
6
6
  import time
7
7
 
8
8
  from .config import get_config
9
+ from .callback_dispatcher import CallbackDispatcher, dispatch_callbacks
9
10
  from .protocol import decode_value, dumps_message, loads_message, new_id, pack_request
10
11
 
11
12
 
@@ -114,9 +115,11 @@ class LTtxRpcClient(object):
114
115
  self._pending_lock = threading.RLock()
115
116
  self._started = False
116
117
  self._recv_thread = None
118
+ self._event_dispatcher = CallbackDispatcher(self._dispatch_event)
117
119
 
118
120
  def start(self):
119
121
  with self._lock:
122
+ self._event_dispatcher.check_request()
120
123
  if self._started:
121
124
  return
122
125
  txl = self._load_txl()
@@ -124,12 +127,14 @@ class LTtxRpcClient(object):
124
127
  self._tx.start_tx()
125
128
  self._tx.start_txg(self.client_id)
126
129
  self._started = True
127
- self._recv_thread = threading.Thread(target=self._recv_loop)
130
+ generation = self._event_dispatcher.start()
131
+ self._recv_thread = threading.Thread(target=self._recv_loop, args=(self._tx, generation))
128
132
  self._recv_thread.daemon = True
129
133
  self._recv_thread.start()
130
134
 
131
135
  def close(self):
132
136
  with self._lock:
137
+ self._event_dispatcher.stop()
133
138
  self._started = False
134
139
  tx = self._tx
135
140
  self._tx = None
@@ -150,39 +155,40 @@ class LTtxRpcClient(object):
150
155
  pass
151
156
  self._pending.clear()
152
157
 
153
- def request(self, action, params=None, timeout=None, request_channel=None):
154
- effective_timeout = float(timeout or self.timeout)
155
- request_id = new_id("req")
156
- raw = pack_request(
157
- action,
158
- params=params,
158
+ def request(self, action, params=None, timeout=None, request_channel=None):
159
+ effective_timeout = float(timeout or self.timeout)
160
+ request_id = new_id("req")
161
+ raw = pack_request(
162
+ action,
163
+ params=params,
159
164
  reply_channel=self.reply_channel,
160
165
  client_id=self.client_id,
161
166
  request_id=request_id,
162
- timeout=effective_timeout,
163
- )
164
- self.start()
167
+ timeout=effective_timeout,
168
+ )
165
169
  q = queue.Queue(maxsize=1)
166
- with self._pending_lock:
167
- self._pending[request_id] = q
168
170
  try:
169
- self._push("request", raw, request_channel or self.request_channel)
170
- try:
171
- msg = q.get(timeout=effective_timeout)
172
- except queue.Empty:
173
- raise CfquantTimeout("cfquant request timeout: %s" % action)
174
- if not msg.get("ok"):
175
- err = msg.get("error") or {}
176
- raise CfquantError(err.get("message") or str(err))
177
- return decode_value(msg.get("result"))
178
- finally:
179
- with self._pending_lock:
180
- self._pending.pop(request_id, None)
181
-
182
- def publish_event(self, channel, payload):
183
- raw = dumps_message(payload)
184
- self.start()
185
- self._push("event", raw, channel)
171
+ with self._lock:
172
+ self.start()
173
+ with self._pending_lock:
174
+ self._pending[request_id] = q
175
+ self._push("request", raw, request_channel or self.request_channel)
176
+ try:
177
+ msg = q.get(timeout=effective_timeout)
178
+ except queue.Empty:
179
+ raise CfquantTimeout("cfquant request timeout: %s" % action)
180
+ if not msg.get("ok"):
181
+ err = msg.get("error") or {}
182
+ raise CfquantError(err.get("message") or str(err))
183
+ return decode_value(msg.get("result"))
184
+ finally:
185
+ with self._pending_lock:
186
+ self._pending.pop(request_id, None)
187
+
188
+ def publish_event(self, channel, payload):
189
+ raw = dumps_message(payload)
190
+ self.start()
191
+ self._push("event", raw, channel)
186
192
 
187
193
  def add_callback(self, event, callback):
188
194
  if callback is None:
@@ -194,11 +200,11 @@ class LTtxRpcClient(object):
194
200
  if callback in callbacks:
195
201
  callbacks.remove(callback)
196
202
 
197
- def _recv_loop(self):
203
+ def _recv_loop(self, expected_tx, generation):
198
204
  while self._started:
199
205
  try:
200
- tx = self._tx
201
- if tx is None:
206
+ tx = expected_tx
207
+ if tx is None or tx is not self._tx:
202
208
  break
203
209
  raw = tx.Q.get()
204
210
  if raw is None:
@@ -206,44 +212,27 @@ class LTtxRpcClient(object):
206
212
  msg = loads_message(raw)
207
213
  if not msg:
208
214
  continue
209
- msg_type = msg.get("type")
210
- if msg_type == "response":
211
- with self._pending_lock:
212
- q = self._pending.pop(msg.get("id"), None)
213
- if q:
214
- q.put(msg)
215
- elif msg_type == "event":
216
- self._dispatch_event(msg)
217
- except Exception:
218
- time.sleep(0.05)
219
- if not self._started:
220
- break
215
+ with self._lock:
216
+ if not self._started or tx is not self._tx:
217
+ return
218
+ msg_type = msg.get("type")
219
+ if msg_type == "response":
220
+ with self._pending_lock:
221
+ q = self._pending.pop(msg.get("id"), None)
222
+ if q:
223
+ q.put(msg)
224
+ elif msg_type == "event":
225
+ self._event_dispatcher.submit(generation, msg)
226
+ except Exception:
227
+ time.sleep(0.05)
228
+ if not self._started:
229
+ break
230
+ with self._lock:
231
+ if self._tx is expected_tx:
232
+ self.close()
221
233
 
222
234
  def _dispatch_event(self, msg):
223
- event = msg.get("event")
224
- data = decode_value(msg.get("data"))
225
- full_msg = dict(msg)
226
- full_msg["data"] = data
227
- for callback in list(self._callbacks.get("__event__", [])):
228
- try:
229
- callback(full_msg)
230
- except Exception:
231
- pass
232
- for callback in list(self._callbacks.get(event, [])):
233
- try:
234
- callback(data)
235
- except Exception:
236
- pass
237
- if event and event.startswith("quote:"):
238
- quote_msg = dict(msg)
239
- quote_msg["data"] = data
240
- if quote_msg.get("subscription_id") is not None and quote_msg.get("subscribe_id") is None:
241
- quote_msg["subscribe_id"] = quote_msg.get("subscription_id")
242
- for callback in list(self._callbacks.get("quote", [])):
243
- try:
244
- callback(quote_msg)
245
- except Exception:
246
- pass
235
+ dispatch_callbacks(self._callbacks, msg, self._event_dispatcher.is_current_callback)
247
236
 
248
237
  def _push(self, key, payload, channel):
249
238
  tx = self._tx
@@ -3,6 +3,7 @@ import queue
3
3
  import threading
4
4
 
5
5
  from .config import get_config
6
+ from .callback_dispatcher import CallbackDispatcher, dispatch_callbacks
6
7
  from .pipe_transport import (
7
8
  DEFAULT_PIPE_NAME,
8
9
  connect_pipe,
@@ -34,9 +35,11 @@ class PipeRpcClient(object):
34
35
  self._pending_lock = threading.RLock()
35
36
  self._started = False
36
37
  self._recv_thread = None
38
+ self._event_dispatcher = CallbackDispatcher(self._dispatch_event)
37
39
 
38
40
  def start(self):
39
41
  with self._lock:
42
+ self._event_dispatcher.check_request()
40
43
  if self._started and self._recv_thread is not None and self._recv_thread.is_alive():
41
44
  return
42
45
  if self._started:
@@ -55,57 +58,60 @@ class PipeRpcClient(object):
55
58
  "client_id": self.client_id,
56
59
  }))
57
60
  self._started = True
58
- self._recv_thread = threading.Thread(target=self._recv_loop, args=(self._rx_conn,))
61
+ generation = self._event_dispatcher.start()
62
+ self._recv_thread = threading.Thread(target=self._recv_loop, args=(self._rx_conn, generation))
59
63
  self._recv_thread.daemon = True
60
64
  self._recv_thread.start()
61
65
 
62
66
  def close(self):
63
67
  with self._lock:
64
- self._started = False
65
- self._close_conns_locked()
66
- self._fail_pending("cfquant pipe client closed")
67
-
68
- def request(self, action, params=None, timeout=None, request_channel=None):
69
- effective_timeout = float(timeout or self.timeout)
70
- request_id = new_id("req")
71
- raw = pack_request(
72
- action,
73
- params=params,
68
+ self._event_dispatcher.stop()
69
+ self._started = False
70
+ self._close_conns_locked()
71
+ self._fail_pending("cfquant pipe client closed")
72
+
73
+ def request(self, action, params=None, timeout=None, request_channel=None):
74
+ effective_timeout = float(timeout or self.timeout)
75
+ request_id = new_id("req")
76
+ raw = pack_request(
77
+ action,
78
+ params=params,
74
79
  reply_channel=self.reply_channel,
75
80
  client_id=self.client_id,
76
81
  request_id=request_id,
77
- timeout=effective_timeout,
78
- )
79
- self.start()
82
+ timeout=effective_timeout,
83
+ )
80
84
  q = queue.Queue(maxsize=1)
81
- with self._pending_lock:
82
- self._pending[request_id] = q
83
85
  try:
84
- try:
85
- self._send_request(raw, request_channel or self.request_channel)
86
- except Exception:
87
- self.close()
88
- raise
89
- try:
90
- msg = q.get(timeout=effective_timeout)
91
- except queue.Empty:
92
- from .client import CfquantTimeout
93
-
94
- raise CfquantTimeout("cfquant pipe request timeout: %s" % action)
95
- if not msg.get("ok"):
96
- err = msg.get("error") or {}
97
- from .client import CfquantError
98
-
99
- raise CfquantError(err.get("message") or str(err))
100
- return decode_value(msg.get("result"))
101
- finally:
102
- with self._pending_lock:
103
- self._pending.pop(request_id, None)
104
-
105
- def publish_event(self, channel, payload):
106
- raw = dumps_message(payload)
107
- self.start()
108
- self._send_request(raw, channel)
86
+ with self._lock:
87
+ self.start()
88
+ with self._pending_lock:
89
+ self._pending[request_id] = q
90
+ try:
91
+ self._send_request(raw, request_channel or self.request_channel)
92
+ except Exception:
93
+ self.close()
94
+ raise
95
+ try:
96
+ msg = q.get(timeout=effective_timeout)
97
+ except queue.Empty:
98
+ from .client import CfquantTimeout
99
+
100
+ raise CfquantTimeout("cfquant pipe request timeout: %s" % action)
101
+ if not msg.get("ok"):
102
+ err = msg.get("error") or {}
103
+ from .client import CfquantError
104
+
105
+ raise CfquantError(err.get("message") or str(err))
106
+ return decode_value(msg.get("result"))
107
+ finally:
108
+ with self._pending_lock:
109
+ self._pending.pop(request_id, None)
110
+
111
+ def publish_event(self, channel, payload):
112
+ raw = dumps_message(payload)
113
+ self.start()
114
+ self._send_request(raw, channel)
109
115
 
110
116
  def add_callback(self, event, callback):
111
117
  if callback is None:
@@ -131,7 +137,7 @@ class PipeRpcClient(object):
131
137
  "payload": payload,
132
138
  }))
133
139
 
134
- def _recv_loop(self, expected_conn):
140
+ def _recv_loop(self, expected_conn, generation):
135
141
  disconnect_message = "cfquant pipe connection closed"
136
142
  while True:
137
143
  try:
@@ -151,14 +157,17 @@ class PipeRpcClient(object):
151
157
  msg = loads_message(payload)
152
158
  if not msg:
153
159
  continue
154
- msg_type = msg.get("type")
155
- if msg_type == "response":
156
- with self._pending_lock:
157
- q = self._pending.pop(msg.get("id"), None)
158
- if q:
159
- q.put(msg)
160
- elif msg_type == "event":
161
- self._dispatch_event(msg)
160
+ with self._lock:
161
+ if not self._started or self._rx_conn is not expected_conn:
162
+ return
163
+ msg_type = msg.get("type")
164
+ if msg_type == "response":
165
+ with self._pending_lock:
166
+ q = self._pending.pop(msg.get("id"), None)
167
+ if q:
168
+ q.put(msg)
169
+ elif msg_type == "event":
170
+ self._event_dispatcher.submit(generation, msg)
162
171
  except Exception as e:
163
172
  disconnect_message = "cfquant pipe receive failed: %s" % e
164
173
  break
@@ -171,8 +180,9 @@ class PipeRpcClient(object):
171
180
  if not self._started and self._rx_conn is None and self._tx_conn is None:
172
181
  return
173
182
  self._started = False
174
- self._close_conns_locked()
175
- self._fail_pending(message)
183
+ self._event_dispatcher.stop()
184
+ self._close_conns_locked()
185
+ self._fail_pending(message)
176
186
 
177
187
  def _close_conns_locked(self):
178
188
  conns = [self._rx_conn, self._tx_conn]
@@ -197,27 +207,4 @@ class PipeRpcClient(object):
197
207
  pass
198
208
 
199
209
  def _dispatch_event(self, msg):
200
- event = msg.get("event")
201
- data = decode_value(msg.get("data"))
202
- full_msg = dict(msg)
203
- full_msg["data"] = data
204
- for callback in list(self._callbacks.get("__event__", [])):
205
- try:
206
- callback(full_msg)
207
- except Exception:
208
- pass
209
- for callback in list(self._callbacks.get(event, [])):
210
- try:
211
- callback(data)
212
- except Exception:
213
- pass
214
- if event and event.startswith("quote:"):
215
- quote_msg = dict(msg)
216
- quote_msg["data"] = data
217
- if quote_msg.get("subscription_id") is not None and quote_msg.get("subscribe_id") is None:
218
- quote_msg["subscribe_id"] = quote_msg.get("subscription_id")
219
- for callback in list(self._callbacks.get("quote", [])):
220
- try:
221
- callback(quote_msg)
222
- except Exception:
223
- pass
210
+ dispatch_callbacks(self._callbacks, msg, self._event_dispatcher.is_current_callback)
@@ -18,7 +18,7 @@ from .level2 import (
18
18
  require_l2_callable,
19
19
  thousand_price,
20
20
  )
21
- from .xttype import filter_cancelable_orders
21
+ from .xttype import _is_zero_time_value, filter_cancelable_orders
22
22
 
23
23
 
24
24
  class CfquantQmtBridge(object):
@@ -1111,23 +1111,26 @@ class CfquantQmtBridge(object):
1111
1111
  "order_time": self._first_value(obj, (
1112
1112
  "time",
1113
1113
  "order_time",
1114
- "entrust_time",
1114
+ "entrust_time",
1115
1115
  "insert_time",
1116
1116
  "m_strOrderTime",
1117
1117
  "m_strEntrustTime",
1118
1118
  "m_strInsertTime",
1119
- "m_nOrderTime",
1120
- "m_nEntrustTime",
1121
- "m_nInsertTime",
1122
- )),
1119
+ "m_nOrderTime",
1120
+ "m_nEntrustTime",
1121
+ "m_nInsertTime",
1122
+ ), skip_zero=True),
1123
1123
  "order_date": self._first_value(obj, (
1124
1124
  "order_date",
1125
1125
  "entrust_date",
1126
+ "insert_date",
1126
1127
  "m_strOrderDate",
1127
1128
  "m_strEntrustDate",
1129
+ "m_strInsertDate",
1128
1130
  "m_strTradingDay",
1129
1131
  "m_nOrderDate",
1130
1132
  "m_nEntrustDate",
1133
+ "m_nInsertDate",
1131
1134
  )),
1132
1135
  "direction": self._get_value(obj, "m_nDirection"),
1133
1136
  "offset_flag": self._get_value(obj, "m_nOffsetFlag"),
@@ -1205,7 +1208,7 @@ class CfquantQmtBridge(object):
1205
1208
  "m_strDealTime",
1206
1209
  "m_nTradeTime",
1207
1210
  "m_nDealTime",
1208
- )),
1211
+ ), skip_zero=True),
1209
1212
  "trade_date": self._first_value(obj, (
1210
1213
  "trade_date",
1211
1214
  "deal_date",
@@ -1377,12 +1380,14 @@ class CfquantQmtBridge(object):
1377
1380
  text = " ".join(str(value or "") for value in values).strip().lower()
1378
1381
  return "cfquant" if "cfquant" in text else "other"
1379
1382
 
1380
- def _first_value(self, obj, names):
1381
- for name in names:
1382
- value = self._get_value(obj, name)
1383
- if value is not None and value != "":
1384
- return value
1385
- return None
1383
+ def _first_value(self, obj, names, skip_zero=False):
1384
+ for name in names:
1385
+ value = self._get_value(obj, name)
1386
+ if value is not None and value != "":
1387
+ if skip_zero and _is_zero_time_value(value):
1388
+ continue
1389
+ return value
1390
+ return None
1386
1391
 
1387
1392
  def _object_to_dict(self, obj):
1388
1393
  if hasattr(obj, "items"):