cfquant 0.2.41__tar.gz → 0.2.42__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 (159) hide show
  1. {cfquant-0.2.41 → cfquant-0.2.42}/PKG-INFO +1 -1
  2. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/normal_bridge.py +131 -1
  3. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/pipe_bridge.py +1 -0
  4. {cfquant-0.2.41 → cfquant-0.2.42}/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 +28 -0
  5. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tx_trade_bridge.py +160 -112
  6. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/version.py +2 -2
  7. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/xttype.py +16 -0
  8. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant.egg-info/PKG-INFO +1 -1
  9. {cfquant-0.2.41 → cfquant-0.2.42}/pyproject.toml +1 -1
  10. {cfquant-0.2.41 → cfquant-0.2.42}/qmt_scripts/CFQUANT.py +5 -4
  11. {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/app.js +1 -1
  12. {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/index.html +2 -2
  13. {cfquant-0.2.41 → cfquant-0.2.42}/LICENSE +0 -0
  14. {cfquant-0.2.41 → cfquant-0.2.42}/LTtx/__init__.py +0 -0
  15. {cfquant-0.2.41 → cfquant-0.2.42}/LTtx/tx/Config.txt +0 -0
  16. {cfquant-0.2.41 → cfquant-0.2.42}/LTtx/tx/LTtx_server.py +0 -0
  17. {cfquant-0.2.41 → cfquant-0.2.42}/LTtx/tx/__init__.py +0 -0
  18. {cfquant-0.2.41 → cfquant-0.2.42}/LTtx/tx/tx.py +0 -0
  19. {cfquant-0.2.41 → cfquant-0.2.42}/MANIFEST.in +0 -0
  20. {cfquant-0.2.41 → cfquant-0.2.42}/README.md +0 -0
  21. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/__init__.py +0 -0
  22. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/_editable_install.py +0 -0
  23. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/account_routing.py +0 -0
  24. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/batch_orders.py +0 -0
  25. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/build_info.py +0 -0
  26. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/callback_dispatcher.py +0 -0
  27. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/cftrader.py +0 -0
  28. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/channels.py +0 -0
  29. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/cli.py +0 -0
  30. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/client.py +0 -0
  31. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/config.py +0 -0
  32. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/hub.py +0 -0
  33. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/level2.py +0 -0
  34. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/log_management.py +0 -0
  35. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/logging_i18n.py +0 -0
  36. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/order_meta.py +0 -0
  37. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/pipe_client.py +0 -0
  38. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/pipe_hub.py +0 -0
  39. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/pipe_transport.py +0 -0
  40. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/protocol.py +0 -0
  41. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/qmt_bridge.py +0 -0
  42. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/qmt_strategy_deploy.py +0 -0
  43. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/qmt_strategy_package.py +0 -0
  44. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/qmt_strategy_runtime.py +0 -0
  45. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/runtime_report.py +0 -0
  46. {cfquant-0.2.41 → cfquant-0.2.42}/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
  47. {cfquant-0.2.41 → cfquant-0.2.42}/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
  48. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/12_/350/256/242/345/215/225/345/244/207/346/263/250/346/265/213/350/257/225.py" +0 -0
  49. {cfquant-0.2.41 → cfquant-0.2.42}/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
  50. {cfquant-0.2.41 → cfquant-0.2.42}/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
  51. {cfquant-0.2.41 → cfquant-0.2.42}/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
  52. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/16_/345/220/257/345/212/250/345/256/211/350/243/205/346/265/213/350/257/225.py" +0 -0
  53. {cfquant-0.2.41 → cfquant-0.2.42}/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
  54. {cfquant-0.2.41 → cfquant-0.2.42}/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
  55. {cfquant-0.2.41 → cfquant-0.2.42}/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
  56. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/1_/350/241/214/346/203/205/346/216/245/346/224/266/346/265/213/350/257/225.py" +0 -0
  57. {cfquant-0.2.41 → cfquant-0.2.42}/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
  58. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/22_/350/277/236/346/216/245/350/257/212/346/226/255/346/265/213/350/257/225.py" +0 -0
  59. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/23_/347/244/272/344/276/213/351/205/215/347/275/256/346/265/213/350/257/225.py" +0 -0
  60. {cfquant-0.2.41 → cfquant-0.2.42}/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
  61. {cfquant-0.2.41 → cfquant-0.2.42}/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
  62. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/26_/345/205/250/346/216/250/350/256/242/351/230/205/346/265/213/350/257/225.py" +0 -0
  63. {cfquant-0.2.41 → cfquant-0.2.42}/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
  64. {cfquant-0.2.41 → cfquant-0.2.42}/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
  65. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/29_Level2/350/241/214/346/203/205/346/265/213/350/257/225.py" +0 -0
  66. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/2_/346/225/260/346/215/256/350/216/267/345/217/226/346/265/213/350/257/225.py" +0 -0
  67. {cfquant-0.2.41 → cfquant-0.2.42}/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
  68. {cfquant-0.2.41 → cfquant-0.2.42}/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
  69. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/32_/345/274/202/346/255/245/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  70. {cfquant-0.2.41 → cfquant-0.2.42}/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
  71. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/3_/346/225/260/346/215/256/344/270/213/350/275/275/346/265/213/350/257/225.py" +0 -0
  72. {cfquant-0.2.41 → cfquant-0.2.42}/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
  73. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/5_/347/234/237/345/256/236/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  74. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/6_/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
  75. {cfquant-0.2.41 → cfquant-0.2.42}/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
  76. {cfquant-0.2.41 → cfquant-0.2.42}/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
  77. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/9_/345/217/257/346/222/244/350/256/242/345/215/225/346/265/213/350/257/225.py" +0 -0
  78. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/_helpers.py +0 -0
  79. {cfquant-0.2.41 → cfquant-0.2.42}/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
  80. {cfquant-0.2.41 → cfquant-0.2.42}/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
  81. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/readme.md +0 -0
  82. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_api_tester_browser.py +0 -0
  83. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_api_tester_web.py +0 -0
  84. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
  85. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_callback_requests.py +0 -0
  86. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_cftrader.py +0 -0
  87. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_cftrader_tutorial.py +0 -0
  88. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_cftrader_web.py +0 -0
  89. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_credit_compat.py +0 -0
  90. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_json_serialization.py +0 -0
  91. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_log_management.py +0 -0
  92. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_management_batch.py +0 -0
  93. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_market_data_fallback.py +0 -0
  94. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_order_time_callback.py +0 -0
  95. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_project_updater.py +0 -0
  96. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_python_api_reference.py +0 -0
  97. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_qmt_strategy_browser.py +0 -0
  98. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_qmt_strategy_deploy.py +0 -0
  99. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_qmt_strategy_web.py +0 -0
  100. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_tutorial_reader.py +0 -0
  101. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tests/test_tx_auto_import.py +0 -0
  102. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/tx.py +0 -0
  103. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/xtconstant.py +0 -0
  104. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/xtdata.py +0 -0
  105. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant/xttrader.py +0 -0
  106. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant.egg-info/SOURCES.txt +0 -0
  107. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant.egg-info/dependency_links.txt +0 -0
  108. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant.egg-info/entry_points.txt +0 -0
  109. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant.egg-info/requires.txt +0 -0
  110. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant.egg-info/top_level.txt +0 -0
  111. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant_pipe_hub.py +0 -0
  112. {cfquant-0.2.41 → cfquant-0.2.42}/cfquant_web_server.py +0 -0
  113. {cfquant-0.2.41 → cfquant-0.2.42}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +0 -0
  114. {cfquant-0.2.41 → cfquant-0.2.42}/qmt_scripts/CFQUANT_LITE.py +0 -0
  115. {cfquant-0.2.41 → cfquant-0.2.42}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
  116. {cfquant-0.2.41 → cfquant-0.2.42}/qmt_scripts/__init__.py +0 -0
  117. {cfquant-0.2.41 → cfquant-0.2.42}/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
  118. {cfquant-0.2.41 → cfquant-0.2.42}/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
  119. {cfquant-0.2.41 → cfquant-0.2.42}/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" +0 -0
  120. {cfquant-0.2.41 → cfquant-0.2.42}/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" +0 -0
  121. {cfquant-0.2.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/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.41 → cfquant-0.2.42}/requirements.txt +0 -0
  125. {cfquant-0.2.41 → cfquant-0.2.42}/setup.cfg +0 -0
  126. {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/__init__.py +0 -0
  127. {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/api-tester.js +0 -0
  128. {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/avatars/amber-pulse.svg +0 -0
  129. {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/avatars/copper-grid.svg +0 -0
  130. {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/avatars/market-blue.svg +0 -0
  131. {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/avatars/rose-circuit.svg +0 -0
  132. {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/avatars/signal-green.svg +0 -0
  133. {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/avatars/slate-wave.svg +0 -0
  134. {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/avatars/teal-orbit.svg +0 -0
  135. {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/avatars/violet-node.svg +0 -0
  136. {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/cftrader-api-data.js +0 -0
  137. {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/favicon.svg +0 -0
  138. {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
  139. {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
  140. {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
  141. {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
  142. {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
  143. {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
  144. {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
  145. {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
  146. {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
  147. {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
  148. {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
  149. {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
  150. {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
  151. {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
  152. {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
  153. {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
  154. {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
  155. {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
  156. {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/python-api-data.js +0 -0
  157. {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/python-api.js +0 -0
  158. {cfquant-0.2.41 → cfquant-0.2.42}/web_dashboard/styles.css +0 -0
  159. {cfquant-0.2.41 → cfquant-0.2.42}/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.41
3
+ Version: 0.2.42
4
4
  Summary: QMT local bridge and xtquant-compatible Python client.
5
5
  License: MIT
6
6
  Requires-Python: >=3.8
@@ -4,6 +4,7 @@ import hashlib
4
4
  import json
5
5
  import os
6
6
  import queue
7
+ import re
7
8
  import threading
8
9
  import time
9
10
 
@@ -91,6 +92,8 @@ class NormalQmtBridge(TxTradeBridge):
91
92
  self.callback_asset_dedupe_max = 4096
92
93
  self.order_terminal_statuses = {}
93
94
  self.order_terminal_statuses_lock = threading.RLock()
95
+ self.pending_order_errors = []
96
+ self.pending_order_errors_lock = threading.RLock()
94
97
 
95
98
  def start(self):
96
99
  if self.running:
@@ -141,6 +144,7 @@ class NormalQmtBridge(TxTradeBridge):
141
144
  self._publish_runtime_report("context_ready")
142
145
 
143
146
  def close(self):
147
+ self._flush_pending_order_errors(force=True)
144
148
  self.running = False
145
149
  self.worker_event.set()
146
150
  self._close_quote_subscriptions()
@@ -627,6 +631,7 @@ class NormalQmtBridge(TxTradeBridge):
627
631
 
628
632
  def on_timer(self, *args, **kwargs):
629
633
  self._maybe_reset_order_meta_stores()
634
+ self._flush_pending_order_errors()
630
635
  if self.dispatch_on_qmt_thread:
631
636
  self._drain_requests("timer")
632
637
  return
@@ -844,10 +849,13 @@ class NormalQmtBridge(TxTradeBridge):
844
849
  def publish_callback_event(self, event_name, obj):
845
850
  if self.tx is None:
846
851
  return
852
+ force_order_error = isinstance(obj, dict) and obj.pop("_cfquant_force_order_error", False)
853
+ reconciled_order_error = None
847
854
  if event_name == "trader:on_stock_order":
848
855
  data = self._format_trade_detail(obj, "order")
849
856
  if not self._accept_order_callback(data):
850
857
  return
858
+ reconciled_order_error = self._match_pending_order_error(data)
851
859
  elif event_name == "trader:on_stock_trade":
852
860
  data = self._format_trade_detail(obj, "deal")
853
861
  else:
@@ -860,6 +868,11 @@ class NormalQmtBridge(TxTradeBridge):
860
868
  data.setdefault("account_id", account_id)
861
869
  if account_type:
862
870
  data.setdefault("account_type", account_type)
871
+ if event_name == "trader:on_order_error":
872
+ self._enrich_qmt_order_error_fields(data)
873
+ if not force_order_error:
874
+ self._queue_pending_order_error(data)
875
+ return
863
876
  if event_name in (
864
877
  "trader:on_stock_order",
865
878
  "trader:on_stock_trade",
@@ -915,6 +928,115 @@ class NormalQmtBridge(TxTradeBridge):
915
928
  "normal bridge callback event sent event=%s account=%s channel_sent=%s clients=%s duplicate=%s"
916
929
  % (event_name, account_id or "-", not channel_duplicate, sent_clients, channel_duplicate and sent_clients == 0)
917
930
  )
931
+ if reconciled_order_error is not None:
932
+ reconciled_order_error.update({
933
+ "order_id": data.get("order_id", -1),
934
+ "m_nOrderID": data.get("m_nOrderID", data.get("order_id", -1)),
935
+ "m_nRef": data.get("m_nRef", data.get("order_id", -1)),
936
+ "order_sysid": data.get("order_sysid", ""),
937
+ "stock_code": data.get("stock_code", reconciled_order_error.get("stock_code", "")),
938
+ "_cfquant_force_order_error": True,
939
+ })
940
+ self.publish_callback_event("trader:on_order_error", reconciled_order_error)
941
+
942
+ def _queue_pending_order_error(self, data):
943
+ with self.pending_order_errors_lock:
944
+ self.pending_order_errors.append({"data": dict(data), "created_at": time.time()})
945
+
946
+ def _match_pending_order_error(self, order):
947
+ if not isinstance(order, dict):
948
+ return None
949
+ account = str(order.get("account_id") or "").strip()
950
+ code = str(order.get("stock_code") or "").upper().split(".", 1)[0]
951
+ strategy = str(order.get("strategy_name") or "")
952
+ remark = str(order.get("order_remark") or "")
953
+ with self.pending_order_errors_lock:
954
+ for index, item in enumerate(self.pending_order_errors):
955
+ error = item.get("data") or {}
956
+ error_account = str(error.get("account_id") or "").strip()
957
+ error_code = str(error.get("stock_code") or "").upper().split(".", 1)[0]
958
+ if account and error_account and account != error_account:
959
+ continue
960
+ if code and error_code and code != error_code:
961
+ continue
962
+ error_strategy = str(error.get("strategy_name") or error.get("strategyName") or "")
963
+ if error_strategy and strategy and error_strategy != strategy and not strategy.startswith(error_strategy + "&&&"):
964
+ continue
965
+ error_remark = str(error.get("order_remark") or error.get("m_strRemark") or "")
966
+ if error_remark and remark and error_remark != remark:
967
+ continue
968
+ self.pending_order_errors.pop(index)
969
+ return error
970
+ return None
971
+
972
+ def _flush_pending_order_errors(self, force=False):
973
+ cutoff = time.time() - 0.5 if not force else time.time() + 1.0
974
+ expired = []
975
+ with self.pending_order_errors_lock:
976
+ keep = []
977
+ for item in self.pending_order_errors:
978
+ if item.get("created_at", 0) <= cutoff:
979
+ expired.append(item.get("data") or {})
980
+ else:
981
+ keep.append(item)
982
+ self.pending_order_errors = keep
983
+ for data in expired:
984
+ data["_cfquant_force_order_error"] = True
985
+ self.publish_callback_event("trader:on_order_error", data)
986
+
987
+ def _enrich_qmt_order_error_fields(self, data):
988
+ """Fill canonical fields that大QMT only embeds in ``errMsg``.
989
+
990
+ The QMT strategy callback is ``orderError_callback(orderArgs, errMsg)``
991
+ and does not expose the MiniQMT ``XtOrderError`` structure. In
992
+ particular, counter errors commonly look like
993
+ ``[COUNTER] [251005][...][p_stock_code=518880,...]``. Preserve any
994
+ native structured fields and only infer missing canonical values.
995
+ """
996
+ if not isinstance(data, dict):
997
+ return data
998
+ # orderError_callback supplies QMT orderArgs names rather than the
999
+ # MiniQMT callback names. Preserve and canonicalize them first.
1000
+ if not data.get("account_id") and data.get("accountID"):
1001
+ data["account_id"] = data["accountID"]
1002
+ if not data.get("stock_code") and data.get("orderCode"):
1003
+ code = str(data["orderCode"]).strip().upper()
1004
+ if code.startswith(("SH", "SZ", "BJ")) and "." not in code:
1005
+ code = "%s.%s" % (code[2:], code[:2])
1006
+ data["stock_code"] = code
1007
+ if not data.get("strategy_name") and data.get("strategyName"):
1008
+ data["strategy_name"] = data["strategyName"]
1009
+ message = data.get("error_msg") or data.get("m_strErrorMsg") or data.get("message") or data.get("msg")
1010
+ if not message:
1011
+ return data
1012
+ message = str(message)
1013
+ if data.get("error_id") in (None, "", 0, "0") and data.get("m_nErrorID") in (None, "", 0, "0") and data.get("error_code") in (None, "", 0, "0"):
1014
+ for token in re.findall(r"\[(\d+)\]", message):
1015
+ try:
1016
+ error_id = int(token)
1017
+ except (TypeError, ValueError):
1018
+ continue
1019
+ if error_id:
1020
+ data["error_id"] = error_id
1021
+ break
1022
+ if not data.get("stock_code"):
1023
+ match = re.search(r"(?:^|[\[,;\s])p_stock_code\s*=\s*([A-Za-z0-9_.-]+)", message, re.IGNORECASE)
1024
+ if match:
1025
+ data["stock_code"] = match.group(1)
1026
+ internal_strategy = data.get("strategy_name") or data.get("strategyName")
1027
+ context = self._consume_order_error_context(
1028
+ data.get("account_id") or data.get("accountID"),
1029
+ data.get("stock_code") or data.get("orderCode"),
1030
+ internal_strategy,
1031
+ ) if internal_strategy else None
1032
+ if context:
1033
+ data["strategy_name"] = context.get("strategy_name", "")
1034
+ data["m_strStrategyName"] = context.get("strategy_name", "")
1035
+ data["strategyName"] = context.get("strategy_name", "")
1036
+ data["order_remark"] = context.get("order_remark", "")
1037
+ data["m_strRemark"] = context.get("order_remark", "")
1038
+ data["cfquant_order_error_context_consumed"] = True
1039
+ return data
918
1040
 
919
1041
  def _accept_order_callback(self, data):
920
1042
  """Filter a stale partial-fill update emitted after a filled update."""
@@ -974,7 +1096,8 @@ class NormalQmtBridge(TxTradeBridge):
974
1096
 
975
1097
  def _callback_object_to_dict(self, obj):
976
1098
  fields = [
977
- "account_id",
1099
+ "account_id",
1100
+ "accountID",
978
1101
  "account_type",
979
1102
  "m_strAccountID",
980
1103
  "m_strAccountId",
@@ -987,6 +1110,7 @@ class NormalQmtBridge(TxTradeBridge):
987
1110
  "order_source",
988
1111
  "source",
989
1112
  "stock_code",
1113
+ "orderCode",
990
1114
  "code",
991
1115
  "market",
992
1116
  "exchange_id",
@@ -1004,6 +1128,12 @@ class NormalQmtBridge(TxTradeBridge):
1004
1128
  "order_remark",
1005
1129
  "remark",
1006
1130
  "strategy_name",
1131
+ "strategyName",
1132
+ "modelPrice",
1133
+ "modelVolume",
1134
+ "opType",
1135
+ "orderType",
1136
+ "prType",
1007
1137
  "trade_id",
1008
1138
  "deal_id",
1009
1139
  "trade_time",
@@ -74,6 +74,7 @@ class PipeNormalQmtBridge(NormalQmtBridge):
74
74
  return self
75
75
 
76
76
  def close(self):
77
+ self._flush_pending_order_errors(force=True)
77
78
  self.running = False
78
79
  self.worker_event.set()
79
80
  self._close_quote_subscriptions()
@@ -451,6 +451,34 @@ def test_order_error_callback_can_match_lightweight_request_before_passorder_ret
451
451
  assert error.error_id == 103
452
452
 
453
453
 
454
+ def test_qmt_order_error_message_fills_missing_error_id_and_stock_code():
455
+ class RecordingTx(object):
456
+ def __init__(self):
457
+ self.pushes = []
458
+
459
+ def push(self, kind, payload, key):
460
+ self.pushes.append((kind, payload, key))
461
+
462
+ bridge = NormalQmtBridge(None, show=False, schedule_timer=False)
463
+ tx = RecordingTx()
464
+ bridge.tx = tx
465
+ try:
466
+ bridge.publish_callback_event("trader:on_order_error", {
467
+ "m_strAccountID": "A123",
468
+ "m_nAccountType": 2,
469
+ "order_source": "other",
470
+ "m_strErrorMsg": "[COUNTER] [251005][证券可用数量不足][p_stock_code=518880,p_enable_amount=0]",
471
+ })
472
+ finally:
473
+ bridge.close()
474
+
475
+ payload = json.loads([item for item in tx.pushes if item[0] == "event"][-1][1])
476
+ data = payload["data"]
477
+ assert data["error_id"] == 251005
478
+ assert data["stock_code"] == "518880"
479
+ assert XtOrderError.from_any(data).error_id == 251005
480
+
481
+
454
482
  @pytest.mark.parametrize("bridge_class", [NormalQmtBridge, PipeNormalQmtBridge])
455
483
  def test_cancel_error_callback_restores_context_by_order_id(bridge_class):
456
484
  class RecordingTx(object):
@@ -3,8 +3,9 @@ import os
3
3
  import inspect
4
4
  import json
5
5
  import sys
6
- import threading
7
- import time
6
+ import threading
7
+ import time
8
+ import uuid
8
9
 
9
10
  from .protocol import loads_message, pack_event, pack_response
10
11
  from .batch_orders import (
@@ -42,53 +43,53 @@ _LOADED_SOURCE_SHA256 = source_sha256(__file__)
42
43
  # bridge against the same ContextInfo. QMT treats every
43
44
  # set_auto_trade_callback(True) call as a new registration, so keep the
44
45
  # registration state shared across bridge instances.
45
- _AUTO_TRADE_CALLBACK_LOCK = threading.RLock()
46
- _AUTO_TRADE_CALLBACK_REGISTRY = {}
47
-
48
- # A normal bridge and a trade bridge can be created for the same QMT context.
49
- # Keep a small relay registry so a stock-order callback received by either
50
- # entry point can wake the synchronous order resolver immediately.
51
- _SYNC_ORDER_CALLBACK_RELAY_LOCK = threading.RLock()
52
- _SYNC_ORDER_CALLBACK_RELAYS = {}
53
-
54
-
55
- def _register_sync_order_callback_relay(bridge):
56
- context = getattr(bridge, "context", None)
57
- if context is None:
58
- return
59
- key = id(context)
60
- with _SYNC_ORDER_CALLBACK_RELAY_LOCK:
61
- bucket = _SYNC_ORDER_CALLBACK_RELAYS.setdefault(key, [])
62
- if bridge not in bucket:
63
- bucket.append(bridge)
64
-
65
-
66
- def _unregister_sync_order_callback_relay(bridge):
67
- context = getattr(bridge, "context", None)
68
- if context is None:
69
- return
70
- key = id(context)
71
- with _SYNC_ORDER_CALLBACK_RELAY_LOCK:
72
- bucket = _SYNC_ORDER_CALLBACK_RELAYS.get(key, [])
73
- bucket[:] = [item for item in bucket if item is not bridge]
74
- if not bucket:
75
- _SYNC_ORDER_CALLBACK_RELAYS.pop(key, None)
76
-
77
-
78
- def relay_sync_order_callback(context, order):
79
- """Forward a formatted order callback to trade bridges sharing context."""
80
- if context is None:
81
- return 0
82
- with _SYNC_ORDER_CALLBACK_RELAY_LOCK:
83
- bridges = list(_SYNC_ORDER_CALLBACK_RELAYS.get(id(context), ()))
84
- matched = 0
85
- for bridge in bridges:
86
- try:
87
- if bridge._resolve_pending_sync_order_callback(order):
88
- matched += 1
89
- except Exception:
90
- continue
91
- return matched
46
+ _AUTO_TRADE_CALLBACK_LOCK = threading.RLock()
47
+ _AUTO_TRADE_CALLBACK_REGISTRY = {}
48
+
49
+ # A normal bridge and a trade bridge can be created for the same QMT context.
50
+ # Keep a small relay registry so a stock-order callback received by either
51
+ # entry point can wake the synchronous order resolver immediately.
52
+ _SYNC_ORDER_CALLBACK_RELAY_LOCK = threading.RLock()
53
+ _SYNC_ORDER_CALLBACK_RELAYS = {}
54
+
55
+
56
+ def _register_sync_order_callback_relay(bridge):
57
+ context = getattr(bridge, "context", None)
58
+ if context is None:
59
+ return
60
+ key = id(context)
61
+ with _SYNC_ORDER_CALLBACK_RELAY_LOCK:
62
+ bucket = _SYNC_ORDER_CALLBACK_RELAYS.setdefault(key, [])
63
+ if bridge not in bucket:
64
+ bucket.append(bridge)
65
+
66
+
67
+ def _unregister_sync_order_callback_relay(bridge):
68
+ context = getattr(bridge, "context", None)
69
+ if context is None:
70
+ return
71
+ key = id(context)
72
+ with _SYNC_ORDER_CALLBACK_RELAY_LOCK:
73
+ bucket = _SYNC_ORDER_CALLBACK_RELAYS.get(key, [])
74
+ bucket[:] = [item for item in bucket if item is not bridge]
75
+ if not bucket:
76
+ _SYNC_ORDER_CALLBACK_RELAYS.pop(key, None)
77
+
78
+
79
+ def relay_sync_order_callback(context, order):
80
+ """Forward a formatted order callback to trade bridges sharing context."""
81
+ if context is None:
82
+ return 0
83
+ with _SYNC_ORDER_CALLBACK_RELAY_LOCK:
84
+ bridges = list(_SYNC_ORDER_CALLBACK_RELAYS.get(id(context), ()))
85
+ matched = 0
86
+ for bridge in bridges:
87
+ try:
88
+ if bridge._resolve_pending_sync_order_callback(order):
89
+ matched += 1
90
+ except Exception:
91
+ continue
92
+ return matched
92
93
 
93
94
 
94
95
  XTTRADER_COMPAT_CANDIDATES = {
@@ -237,22 +238,24 @@ class TxTradeBridge(object):
237
238
  self.pending_async_orders_lock = threading.RLock()
238
239
  self.pending_sync_orders = []
239
240
  self.pending_sync_orders_lock = threading.RLock()
240
- self.order_request_metadata = {}
241
- self.order_request_metadata_lock = threading.RLock()
241
+ self.order_request_metadata = {}
242
+ self.order_request_metadata_lock = threading.RLock()
243
+ self.order_error_contexts = {}
244
+ self.order_error_contexts_lock = threading.RLock()
242
245
  self.order_meta_enabled = bool(order_meta_enabled)
243
246
  self.order_meta_cache = order_meta.OrderMetaCache(self.bridge_id)
244
247
  self.order_meta_store_lock = threading.RLock()
245
248
  self.order_meta_store_initialized = set()
246
249
 
247
- def set_context(self, context):
248
- if self.context is not None and self.context is not context:
249
- self._release_auto_trade_callback(self.context)
250
- _unregister_sync_order_callback_relay(self)
251
- self.context = context
250
+ def set_context(self, context):
251
+ if self.context is not None and self.context is not context:
252
+ self._release_auto_trade_callback(self.context)
253
+ _unregister_sync_order_callback_relay(self)
254
+ self.context = context
252
255
  if self.account_id:
253
256
  self._set_context_account(self.account_id, self.account_type)
254
- self._enable_auto_trade_callback()
255
- _register_sync_order_callback_relay(self)
257
+ self._enable_auto_trade_callback()
258
+ _register_sync_order_callback_relay(self)
256
259
  self._log("tx trade bridge context ready")
257
260
  self._publish_runtime_report("context_ready")
258
261
 
@@ -273,10 +276,10 @@ class TxTradeBridge(object):
273
276
  self._publish_runtime_report("start")
274
277
  return self
275
278
 
276
- def close(self):
277
- self.running = False
278
- self._release_auto_trade_callback()
279
- _unregister_sync_order_callback_relay(self)
279
+ def close(self):
280
+ self.running = False
281
+ self._release_auto_trade_callback()
282
+ _unregister_sync_order_callback_relay(self)
280
283
  tx = self.tx
281
284
  self.tx = None
282
285
  if tx is not None:
@@ -981,7 +984,13 @@ class TxTradeBridge(object):
981
984
  ("order_remark", "remark", "strategy_name"),
982
985
  msg.get("id", "tx_order"),
983
986
  )
984
- strategy_name = params.get("strategy_name", "")
987
+ strategy_name = params.get("strategy_name", "")
988
+ qmt_strategy_name = self._register_order_error_context(
989
+ account_id,
990
+ params.get("stock_code", params.get("code", "")),
991
+ strategy_name,
992
+ order_remark,
993
+ )
985
994
  order_meta_record = None
986
995
  if self.order_meta_enabled:
987
996
  order_meta_record = self._build_order_meta_record(
@@ -1025,12 +1034,13 @@ class TxTradeBridge(object):
1025
1034
  price_type,
1026
1035
  params.get("price", 0),
1027
1036
  params.get("order_volume", params.get("num", 0)),
1028
- params.get("strategy_name", "1"),
1037
+ qmt_strategy_name,
1029
1038
  params.get("quick_trade", 2),
1030
1039
  order_remark,
1031
1040
  self.context,
1032
1041
  )
1033
- except Exception as e:
1042
+ except Exception as e:
1043
+ self._discard_order_error_context(account_id, params.get("stock_code", params.get("code", "")), qmt_strategy_name)
1034
1044
  if pending_sync_order is not None:
1035
1045
  self._discard_pending_sync_order(pending_sync_order)
1036
1046
  if order_meta_record is not None:
@@ -1110,7 +1120,45 @@ class TxTradeBridge(object):
1110
1120
  "previous_order_id": previous_order_id,
1111
1121
  }
1112
1122
 
1113
- def _register_pending_sync_order(
1123
+ @staticmethod
1124
+ def _order_error_code(stock_code):
1125
+ text = str(stock_code or "").strip().upper()
1126
+ if text.startswith(("SH", "SZ", "BJ")) and "." not in text and len(text) > 2:
1127
+ text = text[2:]
1128
+ return text.split(".", 1)[0]
1129
+
1130
+ def _register_order_error_context(self, account_id, stock_code, strategy_name, order_remark):
1131
+ original = str(strategy_name or "")
1132
+ internal = "%s&&&_cfq_%s" % (original, uuid.uuid4().hex[:12]) if original else "cfq_%s" % uuid.uuid4().hex[:12]
1133
+ key = (str(account_id or "").strip(), self._order_error_code(stock_code), internal)
1134
+ now = time.time()
1135
+ with self.order_error_contexts_lock:
1136
+ cutoff = now - 300.0
1137
+ self.order_error_contexts = {
1138
+ item_key: item for item_key, item in self.order_error_contexts.items()
1139
+ if item.get("created_at", now) >= cutoff
1140
+ }
1141
+ self.order_error_contexts[key] = {
1142
+ "account_id": key[0], "stock_code": key[1],
1143
+ "internal_strategy_name": internal, "strategy_name": original,
1144
+ "order_remark": str(order_remark or ""), "created_at": now,
1145
+ "lifecycle": "pending",
1146
+ }
1147
+ while len(self.order_error_contexts) > 4096:
1148
+ self.order_error_contexts.pop(next(iter(self.order_error_contexts)))
1149
+ return internal
1150
+
1151
+ def _discard_order_error_context(self, account_id, stock_code, internal_strategy):
1152
+ key = (str(account_id or "").strip(), self._order_error_code(stock_code), str(internal_strategy or ""))
1153
+ with self.order_error_contexts_lock:
1154
+ self.order_error_contexts.pop(key, None)
1155
+
1156
+ def _consume_order_error_context(self, account_id, stock_code, internal_strategy):
1157
+ key = (str(account_id or "").strip(), self._order_error_code(stock_code), str(internal_strategy or ""))
1158
+ with self.order_error_contexts_lock:
1159
+ return self.order_error_contexts.pop(key, None)
1160
+
1161
+ def _register_pending_sync_order(
1114
1162
  self,
1115
1163
  account_id,
1116
1164
  account_type,
@@ -1129,18 +1177,18 @@ class TxTradeBridge(object):
1129
1177
  "created_at": time.time(),
1130
1178
  "event": threading.Event(),
1131
1179
  }
1132
- with self.pending_sync_orders_lock:
1133
- self.pending_sync_orders.append(record)
1134
- _register_sync_order_callback_relay(self)
1135
- return record
1180
+ with self.pending_sync_orders_lock:
1181
+ self.pending_sync_orders.append(record)
1182
+ _register_sync_order_callback_relay(self)
1183
+ return record
1136
1184
 
1137
1185
  def _discard_pending_sync_order(self, record):
1138
1186
  if record is None:
1139
1187
  return
1140
- with self.pending_sync_orders_lock:
1141
- self.pending_sync_orders[:] = [
1142
- item for item in self.pending_sync_orders if item is not record
1143
- ]
1188
+ with self.pending_sync_orders_lock:
1189
+ self.pending_sync_orders[:] = [
1190
+ item for item in self.pending_sync_orders if item is not record
1191
+ ]
1144
1192
 
1145
1193
  def _resolve_pending_sync_order_callback(self, order):
1146
1194
  """Wake a synchronous resolver when QMT publishes the matching order.
@@ -1175,21 +1223,21 @@ class TxTradeBridge(object):
1175
1223
  expected_strategy = str(record.get("strategy_name") or "")
1176
1224
  if not order_remark and strategy_name and expected_strategy and strategy_name != expected_strategy:
1177
1225
  continue
1178
- if self._is_previous_order_detail(order, record.get("previous_order_id")):
1179
- continue
1180
- # Some QMT builds include the internal reference directly in
1181
- # the callback. Keep it so the synchronous path can return
1182
- # without a full ORDER-history query.
1183
- callback_order_id = None
1184
- for name in ("m_nRef", "m_nOrderID", "order_id"):
1185
- callback_order_id = self._normalize_order_id(self._get_value(order, name))
1186
- if callback_order_id is not None:
1187
- break
1188
- previous_key = self._order_reference_key(record.get("previous_order_id"))
1189
- if callback_order_id is not None and self._order_reference_key(callback_order_id) != previous_key:
1190
- record["callback_order_id"] = callback_order_id
1191
- if order_sysid not in (None, ""):
1192
- record["callback_order_sysid"] = str(order_sysid)
1226
+ if self._is_previous_order_detail(order, record.get("previous_order_id")):
1227
+ continue
1228
+ # Some QMT builds include the internal reference directly in
1229
+ # the callback. Keep it so the synchronous path can return
1230
+ # without a full ORDER-history query.
1231
+ callback_order_id = None
1232
+ for name in ("m_nRef", "m_nOrderID", "order_id"):
1233
+ callback_order_id = self._normalize_order_id(self._get_value(order, name))
1234
+ if callback_order_id is not None:
1235
+ break
1236
+ previous_key = self._order_reference_key(record.get("previous_order_id"))
1237
+ if callback_order_id is not None and self._order_reference_key(callback_order_id) != previous_key:
1238
+ record["callback_order_id"] = callback_order_id
1239
+ if order_sysid not in (None, ""):
1240
+ record["callback_order_sysid"] = str(order_sysid)
1193
1241
  record["callback_seen_at"] = time.time()
1194
1242
  record["event"].set()
1195
1243
  return True
@@ -1218,27 +1266,27 @@ class TxTradeBridge(object):
1218
1266
  wait_seconds = max(0.0, float(wait_seconds or 0))
1219
1267
  except Exception:
1220
1268
  wait_seconds = 2.0
1221
- deadline = time.time() + wait_seconds
1222
- while True:
1223
- if pending_sync_order is not None:
1224
- callback_order_id = pending_sync_order.get("callback_order_id")
1225
- if callback_order_id is not None:
1226
- return callback_order_id
1227
- # A callback is the cheap readiness signal. Waiting for it before
1228
- # querying the complete ORDER list avoids repeatedly transferring
1229
- # multi-megabyte histories through QMT while the new order is still
1230
- # being committed. A zero wait keeps the one-shot lookup behavior
1231
- # used by callers that explicitly disable waiting.
1232
- if (
1233
- pending_sync_order is not None
1234
- and wait_seconds > 0
1235
- and not pending_sync_order.get("callback_seen_at")
1236
- and time.time() < deadline
1237
- ):
1238
- pending_sync_order["event"].wait(min(0.05, max(0.0, deadline - time.time())))
1239
- pending_sync_order["event"].clear()
1240
- continue
1241
- try:
1269
+ deadline = time.time() + wait_seconds
1270
+ while True:
1271
+ if pending_sync_order is not None:
1272
+ callback_order_id = pending_sync_order.get("callback_order_id")
1273
+ if callback_order_id is not None:
1274
+ return callback_order_id
1275
+ # A callback is the cheap readiness signal. Waiting for it before
1276
+ # querying the complete ORDER list avoids repeatedly transferring
1277
+ # multi-megabyte histories through QMT while the new order is still
1278
+ # being committed. A zero wait keeps the one-shot lookup behavior
1279
+ # used by callers that explicitly disable waiting.
1280
+ if (
1281
+ pending_sync_order is not None
1282
+ and wait_seconds > 0
1283
+ and not pending_sync_order.get("callback_seen_at")
1284
+ and time.time() < deadline
1285
+ ):
1286
+ pending_sync_order["event"].wait(min(0.05, max(0.0, deadline - time.time())))
1287
+ pending_sync_order["event"].clear()
1288
+ continue
1289
+ try:
1242
1290
  orders = self._query_trade_detail({
1243
1291
  "account": {"account_id": account_id, "account_type": account_type},
1244
1292
  }, "order")
@@ -1295,8 +1343,8 @@ class TxTradeBridge(object):
1295
1343
  # returned by order queries/callbacks. Wait for the matching detail.
1296
1344
  if time.time() >= deadline:
1297
1345
  return None
1298
- if pending_sync_order is not None:
1299
- pending_sync_order["event"].wait(min(0.05, max(0.0, deadline - time.time())))
1346
+ if pending_sync_order is not None:
1347
+ pending_sync_order["event"].wait(min(0.05, max(0.0, deadline - time.time())))
1300
1348
  pending_sync_order["event"].clear()
1301
1349
  else:
1302
1350
  time.sleep(0.05)
@@ -1,5 +1,5 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """cfquant core version metadata."""
3
3
 
4
- __version__ = "0.2.41"
5
- WEB_VERSION = "web_20260924_02"
4
+ __version__ = "0.2.42"
5
+ WEB_VERSION = "web_20260925_01"
@@ -1209,6 +1209,22 @@ class XtOrderError(DictObject):
1209
1209
  _set_first(data, "strategy_name", ("m_strStrategyName",), default="")
1210
1210
  _set_first(data, "order_remark", ("m_strRemark", "m_strOrderRemark"), default="")
1211
1211
  _normalize_order_id_field(data)
1212
+ # 大 QMT 的 orderError_callback 只提供错误文本,柜台错误码和股票代码
1213
+ # 常以内嵌字段出现,例如 [251005]... [p_stock_code=518880,...]。
1214
+ # 仅在标准字段缺失时补充,保留 MiniQMT 原生字段优先级。
1215
+ if data.get("error_id") in (None, "", 0, "0"):
1216
+ for token in re.findall(r"\[(\d+)\]", str(data.get("error_msg") or "")):
1217
+ if int(token):
1218
+ data["error_id"] = int(token)
1219
+ break
1220
+ if not data.get("stock_code"):
1221
+ match = re.search(
1222
+ r"(?:^|[\[,;\s])p_stock_code\s*=\s*([A-Za-z0-9_.-]+)",
1223
+ str(data.get("error_msg") or ""),
1224
+ re.IGNORECASE,
1225
+ )
1226
+ if match:
1227
+ data["stock_code"] = match.group(1)
1212
1228
  if data.get("order_id") in (0, "0", -1, "-1"):
1213
1229
  data["order_id"] = -1
1214
1230
  data["account_id"] = _coerce_text(data.get("account_id"))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cfquant
3
- Version: 0.2.41
3
+ Version: 0.2.42
4
4
  Summary: QMT local bridge and xtquant-compatible Python client.
5
5
  License: MIT
6
6
  Requires-Python: >=3.8