cfquant 0.2.63__tar.gz → 0.2.64__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 (258) hide show
  1. {cfquant-0.2.63 → cfquant-0.2.64}/PKG-INFO +3 -1
  2. {cfquant-0.2.63 → cfquant-0.2.64}/README.md +2 -0
  3. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/__init__.py +2 -0
  4. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/cftrader.py +66 -1
  5. cfquant-0.2.64/cfquant/conditional_orders.py +149 -0
  6. cfquant-0.2.64/cfquant/planning_broker.py +90 -0
  7. cfquant-0.2.64/cfquant/position_sizing.py +161 -0
  8. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/qmt_bridge.py +5 -0
  9. cfquant-0.2.64/cfquant/qmt_inner.py +308 -0
  10. cfquant-0.2.64/cfquant/qmt_inner_catalog.py +55 -0
  11. cfquant-0.2.64/cfquant/qmt_inner_runtime.py +186 -0
  12. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/qmt_strategy_agent.js +62 -13
  13. cfquant-0.2.64/cfquant/smart_orders.py +512 -0
  14. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_cftrader_tutorial.py +13 -4
  15. cfquant-0.2.64/cfquant/tests/test_qmt_inner.py +334 -0
  16. cfquant-0.2.64/cfquant/tests/test_qmt_inner_reference.py +116 -0
  17. cfquant-0.2.64/cfquant/tests/test_qmt_inner_web.py +62 -0
  18. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_qmt_strategy_capabilities.py +76 -7
  19. cfquant-0.2.64/cfquant/tests/test_smart_orders.py +386 -0
  20. cfquant-0.2.64/cfquant/tests/test_smart_orders_api.py +134 -0
  21. cfquant-0.2.64/cfquant/tests/test_smart_orders_docs.py +65 -0
  22. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_trade_strategy_callback.py +4 -0
  23. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tx_trade_bridge.py +3 -0
  24. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/version.py +2 -2
  25. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant.egg-info/PKG-INFO +3 -1
  26. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant.egg-info/SOURCES.txt +15 -0
  27. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant_web_server.py +61 -3
  28. {cfquant-0.2.63 → cfquant-0.2.64}/pyproject.toml +1 -1
  29. {cfquant-0.2.63 → cfquant-0.2.64}/qmt_scripts/CFQUANT.py +27 -0
  30. {cfquant-0.2.63 → cfquant-0.2.64}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +27 -0
  31. {cfquant-0.2.63 → cfquant-0.2.64}/qmt_scripts/CFQUANT_LITE.py +270 -1
  32. {cfquant-0.2.63 → cfquant-0.2.64}/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 +27 -0
  33. {cfquant-0.2.63 → cfquant-0.2.64}/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 +27 -0
  34. {cfquant-0.2.63 → cfquant-0.2.64}/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 +270 -1
  35. {cfquant-0.2.63 → cfquant-0.2.64}/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 +270 -1
  36. {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/api-tester.js +1 -1
  37. {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/app.js +40 -16
  38. {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/docs.js +2 -2
  39. {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/index.html +16 -6
  40. {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/python-api.js +25 -9
  41. cfquant-0.2.64/web_dashboard/qmt-inner-api-data.js +4211 -0
  42. cfquant-0.2.64/web_dashboard/smart-order-api-data.js +94 -0
  43. {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/styles.css +11 -1
  44. {cfquant-0.2.63 → cfquant-0.2.64}/LICENSE +0 -0
  45. {cfquant-0.2.63 → cfquant-0.2.64}/LTtx/__init__.py +0 -0
  46. {cfquant-0.2.63 → cfquant-0.2.64}/LTtx/tx/Config.txt +0 -0
  47. {cfquant-0.2.63 → cfquant-0.2.64}/LTtx/tx/LTtx_server.py +0 -0
  48. {cfquant-0.2.63 → cfquant-0.2.64}/LTtx/tx/__init__.py +0 -0
  49. {cfquant-0.2.63 → cfquant-0.2.64}/LTtx/tx/tx.py +0 -0
  50. {cfquant-0.2.63 → cfquant-0.2.64}/MANIFEST.in +0 -0
  51. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/_editable_install.py +0 -0
  52. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/account_routing.py +0 -0
  53. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/algo.py +0 -0
  54. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/algo_broker.py +0 -0
  55. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/batch_orders.py +0 -0
  56. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/build_info.py +0 -0
  57. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/callback_dispatcher.py +0 -0
  58. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/channels.py +0 -0
  59. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/cli.py +0 -0
  60. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/client.py +0 -0
  61. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/config.py +0 -0
  62. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/diagnostics.py +0 -0
  63. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/flow_control.py +0 -0
  64. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/hub.py +0 -0
  65. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/level2.py +0 -0
  66. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/log_management.py +0 -0
  67. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/logging_i18n.py +0 -0
  68. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/management.py +0 -0
  69. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/normal_bridge.py +0 -0
  70. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/order_identity.py +0 -0
  71. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/order_meta.py +0 -0
  72. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/pipe_bridge.py +0 -0
  73. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/pipe_client.py +0 -0
  74. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/pipe_hub.py +0 -0
  75. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/pipe_transport.py +0 -0
  76. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/protocol.py +0 -0
  77. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/qmt_binding_defaults.py +0 -0
  78. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/qmt_binding_settings.py +0 -0
  79. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/qmt_component_agent.js +0 -0
  80. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/qmt_component_login.py +0 -0
  81. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/qmt_credentials.py +0 -0
  82. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/qmt_dependencies.py +0 -0
  83. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/qmt_native_files.py +0 -0
  84. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/qmt_native_worker.py +0 -0
  85. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/qmt_pipe_runtime.py +0 -0
  86. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/qmt_strategy_deploy.py +0 -0
  87. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/qmt_strategy_package.py +0 -0
  88. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/qmt_strategy_recovery.py +0 -0
  89. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/qmt_strategy_runtime.py +0 -0
  90. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/qmt_update.py +0 -0
  91. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/quote_history.py +0 -0
  92. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/quote_leases.py +0 -0
  93. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/quote_service.py +0 -0
  94. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/remote_adapter.py +0 -0
  95. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/remote_management.py +0 -0
  96. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/remote_supervisor.py +0 -0
  97. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/rpc_timing.py +0 -0
  98. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/runtime_report.py +0 -0
  99. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/shared_quotes.py +0 -0
  100. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/stock_connect.py +0 -0
  101. {cfquant-0.2.63 → cfquant-0.2.64}/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
  102. {cfquant-0.2.63 → cfquant-0.2.64}/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
  103. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/12_/350/256/242/345/215/225/345/244/207/346/263/250/346/265/213/350/257/225.py" +0 -0
  104. {cfquant-0.2.63 → cfquant-0.2.64}/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
  105. {cfquant-0.2.63 → cfquant-0.2.64}/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
  106. {cfquant-0.2.63 → cfquant-0.2.64}/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
  107. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/16_/345/220/257/345/212/250/345/256/211/350/243/205/346/265/213/350/257/225.py" +0 -0
  108. {cfquant-0.2.63 → cfquant-0.2.64}/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
  109. {cfquant-0.2.63 → cfquant-0.2.64}/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
  110. {cfquant-0.2.63 → cfquant-0.2.64}/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
  111. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/1_/350/241/214/346/203/205/346/216/245/346/224/266/346/265/213/350/257/225.py" +0 -0
  112. {cfquant-0.2.63 → cfquant-0.2.64}/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
  113. {cfquant-0.2.63 → cfquant-0.2.64}/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
  114. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/22_/350/277/236/346/216/245/350/257/212/346/226/255/346/265/213/350/257/225.py" +0 -0
  115. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/23_/347/244/272/344/276/213/351/205/215/347/275/256/346/265/213/350/257/225.py" +0 -0
  116. {cfquant-0.2.63 → cfquant-0.2.64}/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
  117. {cfquant-0.2.63 → cfquant-0.2.64}/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
  118. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/26_/345/205/250/346/216/250/350/256/242/351/230/205/346/265/213/350/257/225.py" +0 -0
  119. {cfquant-0.2.63 → cfquant-0.2.64}/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
  120. {cfquant-0.2.63 → cfquant-0.2.64}/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
  121. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/29_Level2/350/241/214/346/203/205/346/265/213/350/257/225.py" +0 -0
  122. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/2_/346/225/260/346/215/256/350/216/267/345/217/226/346/265/213/350/257/225.py" +0 -0
  123. {cfquant-0.2.63 → cfquant-0.2.64}/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
  124. {cfquant-0.2.63 → cfquant-0.2.64}/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
  125. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/32_/345/274/202/346/255/245/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  126. {cfquant-0.2.63 → cfquant-0.2.64}/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
  127. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/34_/345/205/250/346/216/250/345/206/205/351/203/250/345/273/266/350/277/237/346/265/213/350/257/225.py" +0 -0
  128. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/3_/346/225/260/346/215/256/344/270/213/350/275/275/346/265/213/350/257/225.py" +0 -0
  129. {cfquant-0.2.63 → cfquant-0.2.64}/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
  130. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/5_/347/234/237/345/256/236/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  131. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/6_/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
  132. {cfquant-0.2.63 → cfquant-0.2.64}/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
  133. {cfquant-0.2.63 → cfquant-0.2.64}/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
  134. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/9_/345/217/257/346/222/244/350/256/242/345/215/225/346/265/213/350/257/225.py" +0 -0
  135. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/_helpers.py +0 -0
  136. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/readme.md +0 -0
  137. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_account_query_timing.py +0 -0
  138. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_account_subscription_probe.py +0 -0
  139. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_advanced_parity.py +0 -0
  140. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_algo.py +0 -0
  141. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_algo_broker.py +0 -0
  142. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_api_tester_browser.py +0 -0
  143. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_api_tester_web.py +0 -0
  144. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
  145. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_binding_live_refresh_browser.py +0 -0
  146. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_binding_runtime_browser.py +0 -0
  147. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_callback_recovery.py +0 -0
  148. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_callback_requests.py +0 -0
  149. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_cftrader.py +0 -0
  150. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_cftrader_web.py +0 -0
  151. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_credit_compat.py +0 -0
  152. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_dataframe_encoding.py +0 -0
  153. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_json_serialization.py +0 -0
  154. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_latency_flow_control.py +0 -0
  155. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_lite_account_routing.py +0 -0
  156. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_lite_deployment_paths.py +0 -0
  157. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_lite_parity.py +0 -0
  158. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_lite_qmt_dispatch.py +0 -0
  159. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_local_trading_calendar.py +0 -0
  160. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_log_management.py +0 -0
  161. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_management.py +0 -0
  162. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_management_batch.py +0 -0
  163. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_market_data_fallback.py +0 -0
  164. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_market_timing.py +0 -0
  165. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_order_identity.py +0 -0
  166. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_order_time_callback.py +0 -0
  167. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_pipe_shutdown.py +0 -0
  168. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_project_updater.py +0 -0
  169. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_python_api_reference.py +0 -0
  170. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_qmt_auto_login_browser.py +0 -0
  171. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_qmt_auto_login_integration.py +0 -0
  172. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_qmt_binding_defaults.py +0 -0
  173. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_qmt_binding_settings.py +0 -0
  174. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_qmt_component_login.py +0 -0
  175. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_qmt_dependencies.py +0 -0
  176. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_qmt_fileless.py +0 -0
  177. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_qmt_native_files.py +0 -0
  178. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_qmt_native_reuse.py +0 -0
  179. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_qmt_native_worker.py +0 -0
  180. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_qmt_strategy_browser.py +0 -0
  181. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_qmt_strategy_deploy.py +0 -0
  182. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_qmt_strategy_recovery.py +0 -0
  183. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_qmt_strategy_web.py +0 -0
  184. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_qmt_update.py +0 -0
  185. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_quote_latency.py +0 -0
  186. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_quote_latency_encoding.py +0 -0
  187. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_quote_service.py +0 -0
  188. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_remote_management.py +0 -0
  189. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_remote_management_browser.py +0 -0
  190. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_remote_version_check.py +0 -0
  191. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
  192. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_shared_quotes.py +0 -0
  193. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_shared_quotes_generated.py +0 -0
  194. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_startup_recovery.py +0 -0
  195. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_stock_connect.py +0 -0
  196. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_stock_connect_browser.py +0 -0
  197. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_strategy_recovery_api.py +0 -0
  198. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_subscription_congestion.py +0 -0
  199. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_table_conversion.py +0 -0
  200. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_trader_subscription_recovery.py +0 -0
  201. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_trading_dates.py +0 -0
  202. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_trading_monitor.py +0 -0
  203. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_trading_monitor_browser.py +0 -0
  204. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_tutorial_reader.py +0 -0
  205. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_tx_auto_import.py +0 -0
  206. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_tx_shutdown.py +0 -0
  207. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/trading_calendar.py +0 -0
  208. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/trading_monitor.py +0 -0
  209. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tx.py +0 -0
  210. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/xtconstant.py +0 -0
  211. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/xtdata.py +0 -0
  212. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/xttrader.py +0 -0
  213. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/xttype.py +0 -0
  214. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant.egg-info/dependency_links.txt +0 -0
  215. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant.egg-info/entry_points.txt +0 -0
  216. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant.egg-info/requires.txt +0 -0
  217. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant.egg-info/top_level.txt +0 -0
  218. {cfquant-0.2.63 → cfquant-0.2.64}/cfquant_pipe_hub.py +0 -0
  219. {cfquant-0.2.63 → cfquant-0.2.64}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
  220. {cfquant-0.2.63 → cfquant-0.2.64}/qmt_scripts/__init__.py +0 -0
  221. {cfquant-0.2.63 → cfquant-0.2.64}/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
  222. {cfquant-0.2.63 → cfquant-0.2.64}/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
  223. {cfquant-0.2.63 → cfquant-0.2.64}/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
  224. {cfquant-0.2.63 → cfquant-0.2.64}/requirements.txt +0 -0
  225. {cfquant-0.2.63 → cfquant-0.2.64}/setup.cfg +0 -0
  226. {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/__init__.py +0 -0
  227. {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/avatars/amber-pulse.svg +0 -0
  228. {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/avatars/copper-grid.svg +0 -0
  229. {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/avatars/market-blue.svg +0 -0
  230. {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/avatars/rose-circuit.svg +0 -0
  231. {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/avatars/signal-green.svg +0 -0
  232. {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/avatars/slate-wave.svg +0 -0
  233. {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/avatars/teal-orbit.svg +0 -0
  234. {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/avatars/violet-node.svg +0 -0
  235. {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/cftrader-api-data.js +0 -0
  236. {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/favicon.svg +0 -0
  237. {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
  238. {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
  239. {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
  240. {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
  241. {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
  242. {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
  243. {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
  244. {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
  245. {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
  246. {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
  247. {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
  248. {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
  249. {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
  250. {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
  251. {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
  252. {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
  253. {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
  254. {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
  255. {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/python-api-data.js +0 -0
  256. {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/remote-management.js +0 -0
  257. {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/trading-monitor.js +0 -0
  258. {cfquant-0.2.63 → cfquant-0.2.64}/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.63
3
+ Version: 0.2.64
4
4
  Summary: QMT local bridge and xtquant-compatible Python client.
5
5
  License: MIT
6
6
  Requires-Python: >=3.8
@@ -48,6 +48,7 @@ Requires-Dist: pyzmq>=25; extra == "zmq"
48
48
  |---|---|
49
49
  | MiniQMT / `xtquant` 兼容调用 | 原有外部 Python 策略可以沿用接近 `xtdata`、`XtQuantTrader`、`StockAccount` 的写法,降低迁移成本。 |
50
50
  | 大 QMT 本地桥接 | 把大 QMT 内部的行情、查询、交易、撤单和回调能力桥接给外部 Python 程序和 Web 控制台。 |
51
+ | 大 QMT 原生接口类 | 独立的 `QmtInnerApi` 面向外部数据查询、研究和实盘交易,按系统、行情、交易、回调、扩展数据与因子细分,保留原生函数名与参数,详见 [内置接口说明](docs/大QMT内置接口.md)。 |
51
52
  | Web 控制台 | 在浏览器里完成账号绑定、QMT 入口脚本指引、在线检测、接口调试、版本更新和回滚。 |
52
53
  | 多账号 / 多 QMT 路由 | 按 `bridge_id`、`account_type`、`account_id` 路由请求,适配多资金账号、普通账户、信用账户和多 QMT 终端。 |
53
54
  | 通用、极致、高级三种模式 | 普通环境快速跑通;受限 QMT 使用自包含入口;低延迟场景可拆分普通桥和交易桥。 |
@@ -397,6 +398,7 @@ restart_cfquant.bat 重启
397
398
  | QMT 综合部署教程 | [QMT 部署教程](docs/QMT部署教程.md) |
398
399
  | cftrader 批量同步/异步下单与撤单 | [cftrader 批量交易与撤单](docs/cftrader批量交易.md),含 100 单本地基准 |
399
400
  | cftrader TWAP / VWAP / POV | [算法交易与共享全推行情](docs/algorithm_trading.md),含自动订阅、生命周期和接口示例 |
401
+ | 条件单 / 仓位计算 / 下单计划 | [条件单、仓位计算与下单计划](docs/conditional_orders.md),含止盈止损、到时平仓、回撤 / 保本保护、风险仓位及 Python / HTTP 接口 |
400
402
  | 通用模式部署 | [通用模式部署指南](docs/通用模式部署指南.md) |
401
403
  | 极致模式部署 | [极致模式部署指南](docs/极致模式部署指南.md) |
402
404
  | 高级模式部署 | [高级模式部署指南](docs/高级模式部署指南.md) |
@@ -23,6 +23,7 @@
23
23
  |---|---|
24
24
  | MiniQMT / `xtquant` 兼容调用 | 原有外部 Python 策略可以沿用接近 `xtdata`、`XtQuantTrader`、`StockAccount` 的写法,降低迁移成本。 |
25
25
  | 大 QMT 本地桥接 | 把大 QMT 内部的行情、查询、交易、撤单和回调能力桥接给外部 Python 程序和 Web 控制台。 |
26
+ | 大 QMT 原生接口类 | 独立的 `QmtInnerApi` 面向外部数据查询、研究和实盘交易,按系统、行情、交易、回调、扩展数据与因子细分,保留原生函数名与参数,详见 [内置接口说明](docs/大QMT内置接口.md)。 |
26
27
  | Web 控制台 | 在浏览器里完成账号绑定、QMT 入口脚本指引、在线检测、接口调试、版本更新和回滚。 |
27
28
  | 多账号 / 多 QMT 路由 | 按 `bridge_id`、`account_type`、`account_id` 路由请求,适配多资金账号、普通账户、信用账户和多 QMT 终端。 |
28
29
  | 通用、极致、高级三种模式 | 普通环境快速跑通;受限 QMT 使用自包含入口;低延迟场景可拆分普通桥和交易桥。 |
@@ -372,6 +373,7 @@ restart_cfquant.bat 重启
372
373
  | QMT 综合部署教程 | [QMT 部署教程](docs/QMT部署教程.md) |
373
374
  | cftrader 批量同步/异步下单与撤单 | [cftrader 批量交易与撤单](docs/cftrader批量交易.md),含 100 单本地基准 |
374
375
  | cftrader TWAP / VWAP / POV | [算法交易与共享全推行情](docs/algorithm_trading.md),含自动订阅、生命周期和接口示例 |
376
+ | 条件单 / 仓位计算 / 下单计划 | [条件单、仓位计算与下单计划](docs/conditional_orders.md),含止盈止损、到时平仓、回撤 / 保本保护、风险仓位及 Python / HTTP 接口 |
375
377
  | 通用模式部署 | [通用模式部署指南](docs/通用模式部署指南.md) |
376
378
  | 极致模式部署 | [极致模式部署指南](docs/极致模式部署指南.md) |
377
379
  | 高级模式部署 | [高级模式部署指南](docs/高级模式部署指南.md) |
@@ -11,6 +11,7 @@ cfquant: 面向 QMT 的 xtquant 兼容层。
11
11
 
12
12
  from . import xtconstant, xtdata, xttrader, xttype
13
13
  from . import cftrader
14
+ from .qmt_inner import QmtInnerApi
14
15
  from .client import CfquantError, CfquantTimeout, configure, get_client
15
16
  from .version import __version__
16
17
  from .build_info import build_identity, get_git_commit
@@ -25,6 +26,7 @@ __all__ = [
25
26
  "xtdata",
26
27
  "xttrader",
27
28
  "cftrader",
29
+ "QmtInnerApi",
28
30
  "xttype",
29
31
  "configure",
30
32
  "get_client",
@@ -5,6 +5,7 @@ from .batch_orders import (
5
5
  prepare_batch_cancels, prepare_batch_orders, validate_batch_cancel_response, validate_batch_response,
6
6
  )
7
7
  from .protocol import new_id
8
+ from .position_sizing import calculate_position
8
9
  from .xttrader import XtQuantTrader, _account_payload
9
10
  import threading
10
11
 
@@ -26,7 +27,7 @@ def _algo_rpc(action, params):
26
27
  return client.request(action, params)
27
28
 
28
29
 
29
- __all__ = ["CfQuantTrader"]
30
+ __all__ = ["CfQuantTrader", "calculate_position"]
30
31
 
31
32
  class CfQuantTrader:
32
33
  """Use CfQuantTrader(trader) with an existing cfquant XtQuantTrader instance.
@@ -57,6 +58,70 @@ class CfQuantTrader:
57
58
  def order_twap(self, account, stock_code, order_type, order_volume, limit_price, **options):
58
59
  return self.order_algo(account, stock_code, order_type, order_volume, "TWAP", limit_price, **options)
59
60
 
61
+ calculate_position = staticmethod(calculate_position)
62
+
63
+ def order_condition(self, account, stock_code, conditions, execution, **options):
64
+ """Create a persistent sell protection group; manage it with query/pause/cancel_algo."""
65
+ payload = _account_payload(self._trader._resolve_account(account))
66
+ params = dict(options, account=payload, stock_code=stock_code, conditions=conditions, execution=execution)
67
+ params.setdefault("request_key", new_id("condition_request"))
68
+ task_id = _algo_rpc("cftrader.order_condition", params)
69
+ self._algo_accounts[task_id] = payload
70
+ return task_id
71
+
72
+ def order_stop_loss(self, account, stock_code, order_volume, stop_price, limit_price, **options):
73
+ return self.order_condition(account, stock_code, [{"type": "stop_loss", "price": stop_price}],
74
+ {"limit_price": limit_price}, order_volume=order_volume, **options)
75
+
76
+ def order_take_profit(self, account, stock_code, order_volume, take_profit_price, limit_price, **options):
77
+ return self.order_condition(account, stock_code, [{"type": "take_profit", "price": take_profit_price}],
78
+ {"limit_price": limit_price}, order_volume=order_volume, **options)
79
+
80
+ def order_trailing_stop(self, account, stock_code, order_volume, drawdown_percent, limit_price,
81
+ activation_price=None, **options):
82
+ rule = dict(type="trailing_stop", drawdown_percent=drawdown_percent)
83
+ if activation_price is not None:
84
+ rule["activation_price"] = activation_price
85
+ return self.order_condition(account, stock_code, [rule], {"limit_price": limit_price},
86
+ order_volume=order_volume, **options)
87
+
88
+ def order_breakeven_stop(self, account, stock_code, order_volume, reference_price, activation_percent,
89
+ limit_price, offset_amount=0, **options):
90
+ return self.order_condition(account, stock_code,
91
+ [dict(type="breakeven_stop", activation_percent=activation_percent, offset_amount=offset_amount)],
92
+ {"limit_price": limit_price}, order_volume=order_volume, reference_price=reference_price, **options)
93
+
94
+ def order_time_exit(self, account, stock_code, order_volume, at, limit_price, only_if_loss=False, **options):
95
+ return self.order_condition(account, stock_code, [dict(type="time_exit", at=at, only_if_loss=only_if_loss)],
96
+ {"limit_price": limit_price}, order_volume=order_volume, **options)
97
+
98
+ def preview_order_plan(self, account, stock_code, sizing, **options):
99
+ """Persist a preview without ordering; account balances are read by the service."""
100
+ payload = _account_payload(self._trader._resolve_account(account))
101
+ params = dict(options, account=payload, stock_code=stock_code, sizing=sizing)
102
+ params.setdefault("request_key", new_id("plan_request"))
103
+ plan = _algo_rpc("cftrader.preview_order_plan", params)
104
+ self._algo_accounts[plan["algo_id"]] = payload
105
+ return plan
106
+
107
+ def _plan_control(self, action, plan_id=None, account=None):
108
+ payload = _account_payload(self._trader._resolve_account(account or self._algo_accounts.get(plan_id)))
109
+ return _algo_rpc("cftrader." + action, dict(plan_id=plan_id, account=payload))
110
+
111
+ def submit_order_plan(self, plan_id, account=None):
112
+ """Revalidate and submit once. Repeating the same plan_id returns its current state."""
113
+ return self._plan_control("submit_order_plan", plan_id, account)
114
+
115
+ def query_order_plan(self, plan_id, account=None):
116
+ return self._plan_control("query_order_plan", plan_id, account)
117
+
118
+ def list_order_plans(self, account=None):
119
+ return self._plan_control("list_order_plans", account=account)
120
+
121
+ def cancel_order_plan(self, plan_id, account=None):
122
+ """Cancel the entry remainder; retain protection for confirmed fills."""
123
+ return self._plan_control("cancel_order_plan", plan_id, account)
124
+
60
125
  def order_vwap(self, account, stock_code, order_type, order_volume, limit_price, **options):
61
126
  return self.order_algo(account, stock_code, order_type, order_volume, "VWAP", limit_price, **options)
62
127
 
@@ -0,0 +1,149 @@
1
+ """Validated, serializable conditions evaluated against observed quote prices."""
2
+ import copy
3
+ import math
4
+
5
+ from .algo import positive
6
+ from .algo_broker import execution_time
7
+
8
+
9
+ def validate_conditions(conditions, reference_price=None):
10
+ if not isinstance(conditions, list) or not 1 <= len(conditions) <= 32:
11
+ raise ValueError("conditions must contain 1 to 32 rules (any matching rule triggers the group)")
12
+ reference = positive(reference_price, "reference_price") if reference_price is not None else None
13
+ result = []
14
+ fields = {
15
+ "stop_loss": {"price", "percent"}, "take_profit": {"price", "percent"},
16
+ "time_exit": {"at", "only_if_loss"}, "holding_exit": {"seconds", "trading_days", "opened_at", "exit_time"},
17
+ "trailing_stop": {"drawdown_percent", "drawdown_amount", "activation_price", "activation_percent"},
18
+ "breakeven_stop": {"activation_percent", "activation_price", "offset_percent", "offset_amount", "protect_price"},
19
+ "step_stop": {"steps"},
20
+ }
21
+ for index, original in enumerate(conditions):
22
+ if not isinstance(original, dict):
23
+ raise ValueError("each condition must be an object")
24
+ rule = copy.deepcopy(original)
25
+ kind = rule.get("type")
26
+ if kind not in fields or set(rule) - (fields.get(kind, set()) | {"type", "name"}):
27
+ raise ValueError("unknown condition type or fields")
28
+ rule.setdefault("name", "%s_%d" % (kind, index + 1))
29
+ if not isinstance(rule["name"], str) or not rule["name"]:
30
+ raise ValueError("condition name must be nonempty text")
31
+ if kind in ("stop_loss", "take_profit"):
32
+ if ("price" in rule) == ("percent" in rule):
33
+ raise ValueError(kind + " requires exactly one of price or percent")
34
+ if "percent" in rule:
35
+ pct = positive(rule["percent"], "percent")
36
+ if reference is None or (kind == "stop_loss" and pct >= 1):
37
+ raise ValueError("percentage conditions require reference_price; stop percent must be below 1")
38
+ rule["price"] = reference * (1 + pct if kind == "take_profit" else 1 - pct)
39
+ rule["price"] = positive(rule["price"], "condition price")
40
+ elif kind == "time_exit":
41
+ rule["at"] = execution_time(rule.get("at"))
42
+ if rule["at"] is None:
43
+ raise ValueError("time_exit requires at")
44
+ if not isinstance(rule.get("only_if_loss", False), bool):
45
+ raise ValueError("only_if_loss must be boolean")
46
+ if rule.get("only_if_loss") and reference is None:
47
+ raise ValueError("only_if_loss requires reference_price")
48
+ elif kind == "holding_exit":
49
+ if ("seconds" in rule) == ("trading_days" in rule):
50
+ raise ValueError("holding_exit requires exactly one of seconds or trading_days")
51
+ rule["opened_at"] = execution_time(rule.get("opened_at"))
52
+ if rule["opened_at"] is None:
53
+ raise ValueError("holding_exit requires opened_at")
54
+ if "seconds" in rule:
55
+ rule["at"] = rule["opened_at"] + positive(rule["seconds"], "seconds")
56
+ else:
57
+ rule["trading_days"] = positive(rule["trading_days"], "trading_days", integer=True)
58
+ if rule["trading_days"] > 2500:
59
+ raise ValueError("trading_days must not exceed 2500")
60
+ import datetime
61
+ datetime.time.fromisoformat(rule.setdefault("exit_time", "14:50"))
62
+ elif kind in ("trailing_stop", "breakeven_stop"):
63
+ if "activation_price" in rule and "activation_percent" in rule:
64
+ raise ValueError("use one activation threshold")
65
+ if "activation_percent" in rule:
66
+ if reference is None:
67
+ raise ValueError("activation_percent requires reference_price")
68
+ rule["activation_price"] = reference * (1 + positive(rule["activation_percent"], "activation_percent", allow_zero=True))
69
+ if "activation_price" in rule:
70
+ rule["activation_price"] = positive(rule["activation_price"], "activation_price")
71
+ if kind == "trailing_stop":
72
+ if ("drawdown_percent" in rule) == ("drawdown_amount" in rule):
73
+ raise ValueError("trailing_stop requires exactly one drawdown threshold")
74
+ key = "drawdown_percent" if "drawdown_percent" in rule else "drawdown_amount"
75
+ rule[key] = positive(rule[key], key)
76
+ if key == "drawdown_percent" and rule[key] >= 1:
77
+ raise ValueError("drawdown_percent must be below 1")
78
+ else:
79
+ if "activation_price" not in rule:
80
+ raise ValueError("breakeven_stop requires activation_price or activation_percent")
81
+ offsets = set(rule) & {"offset_amount", "offset_percent", "protect_price"}
82
+ if len(offsets) > 1:
83
+ raise ValueError("use one breakeven protection offset")
84
+ if "protect_price" not in rule:
85
+ if reference is None:
86
+ raise ValueError("breakeven_stop requires reference_price or protect_price")
87
+ rule["protect_price"] = reference + (reference * positive(rule["offset_percent"], "offset_percent", allow_zero=True)
88
+ if "offset_percent" in rule else positive(rule.get("offset_amount", 0), "offset_amount", allow_zero=True))
89
+ rule["protect_price"] = positive(rule["protect_price"], "protect_price")
90
+ if rule["protect_price"] >= rule["activation_price"]:
91
+ raise ValueError("protect_price must be below activation_price")
92
+ elif kind == "step_stop":
93
+ if reference is None or not isinstance(rule.get("steps"), list) or not rule["steps"]:
94
+ raise ValueError("step_stop requires reference_price and steps")
95
+ previous_gain = previous_stop = -1
96
+ for step in rule["steps"]:
97
+ if not isinstance(step, dict) or set(step) != {"gain_percent", "protect_percent"}:
98
+ raise ValueError("each step requires gain_percent and protect_percent")
99
+ gain = positive(step["gain_percent"], "gain_percent", allow_zero=True)
100
+ protect = positive(step["protect_percent"], "protect_percent", allow_zero=True)
101
+ if gain <= previous_gain or protect < previous_stop or protect >= gain:
102
+ raise ValueError("steps must have increasing gains and nondecreasing protection below each gain")
103
+ previous_gain, previous_stop = gain, protect
104
+ result.append(rule)
105
+ return result
106
+
107
+
108
+ def evaluate_conditions(rules, states, price, now, reference, trigger_on_create=True):
109
+ """Mutate persisted states; return the first trigger. Observed highs never fall."""
110
+ triggered = None
111
+ def above(value, threshold):
112
+ return value >= threshold or math.isclose(value, threshold, rel_tol=1e-12, abs_tol=1e-10)
113
+ def below(value, threshold):
114
+ return value <= threshold or math.isclose(value, threshold, rel_tol=1e-12, abs_tol=1e-10)
115
+ for index, rule in enumerate(rules):
116
+ state = states.setdefault(str(index), {})
117
+ kind = rule["type"]
118
+ hit = False
119
+ if kind in ("stop_loss", "take_profit"):
120
+ hit = below(price, rule["price"]) if kind == "stop_loss" else above(price, rule["price"])
121
+ elif kind in ("time_exit", "holding_exit"):
122
+ if now >= rule["at"]:
123
+ # Evaluate the loss gate once, at the first executable fresh snapshot.
124
+ if "time_decision" not in state:
125
+ state["time_decision"] = not rule.get("only_if_loss") or price < reference
126
+ hit = state["time_decision"]
127
+ elif kind in ("trailing_stop", "breakeven_stop"):
128
+ if above(price, rule.get("activation_price", 0)):
129
+ state.setdefault("activated_at", now)
130
+ if "activated_at" in state:
131
+ if kind == "trailing_stop":
132
+ state["high"] = max(state.get("high", price), price)
133
+ candidate = (state["high"] * (1 - rule["drawdown_percent"]) if "drawdown_percent" in rule
134
+ else state["high"] - rule["drawdown_amount"])
135
+ else:
136
+ candidate = rule["protect_price"]
137
+ state["stop"] = max(state.get("stop", candidate), candidate)
138
+ hit = below(price, state["stop"])
139
+ else:
140
+ for step in rule["steps"]:
141
+ if above(price, reference * (1 + step["gain_percent"])):
142
+ state["stop"] = max(state.get("stop", 0), reference * (1 + step["protect_percent"]))
143
+ hit = "stop" in state and below(price, state["stop"])
144
+ if not hit:
145
+ state["armed"] = True
146
+ if hit and (trigger_on_create or state.get("armed") or kind in ("time_exit", "holding_exit")) and triggered is None:
147
+ triggered = dict(index=index, name=rule["name"], type=kind, price=price, at=now,
148
+ threshold=state.get("stop", rule.get("price", rule.get("at"))))
149
+ return triggered
@@ -0,0 +1,90 @@
1
+ """Account snapshots and exchange calendars used by persistent order plans."""
2
+ import datetime as dt
3
+ import time
4
+
5
+ from .algo import positive
6
+ from .algo_broker import RoutedAlgoBroker, SHANGHAI
7
+ from .diagnostics import quote_time_ms
8
+ from .position_sizing import instrument_rules
9
+ from .xttype import XtAsset, XtPosition
10
+
11
+
12
+ class PlanningBroker(RoutedAlgoBroker):
13
+ def __init__(self, *args, **kwargs):
14
+ super().__init__(*args, **kwargs)
15
+ self._calendar = {}
16
+
17
+ def rules(self, code):
18
+ detail = self.market_request("xtdata.get_instrument_detail", dict(stock_code=code, iscomplete=True))
19
+ if not isinstance(detail, dict):
20
+ raise ValueError("instrument metadata unavailable")
21
+ return instrument_rules(code, price_tick=positive(detail.get("PriceTick"), "PriceTick"))
22
+
23
+ def snapshot(self, account, code):
24
+ params = dict(account=account, stock_code=code)
25
+ asset = self.trade_request(account, "xttrader.query_stock_asset", params)
26
+ if isinstance(asset, list):
27
+ asset = asset[0] if len(asset) == 1 else None
28
+ if asset is None:
29
+ raise RuntimeError("account assets unavailable")
30
+ asset = XtAsset.from_any(asset)
31
+ rows = self.trade_request(account, "xttrader.query_stock_positions", params)
32
+ if not isinstance(rows, (list, tuple)):
33
+ raise RuntimeError("account positions unavailable")
34
+ positions = [XtPosition.from_any(row) for row in rows]
35
+ positions = [row for row in positions if row.stock_code == code]
36
+ orders = self.orders(account, code)
37
+ pending = sum(max(0, int(row.get("order_volume") or 0) - int(row.get("traded_volume") or 0))
38
+ for row in orders if row.get("stock_code") == code and int(row.get("order_type") or 0) == 23
39
+ and int(row.get("order_status", 255)) not in (53, 54, 56, 57))
40
+ return dict(total_asset=positive(asset.total_asset, "total_asset", allow_zero=True),
41
+ available_cash=positive(asset.cash, "available_cash", allow_zero=True),
42
+ current_volume=sum(int(row.volume) for row in positions),
43
+ sellable_volume=sum(int(row.can_use_volume) for row in positions),
44
+ pending_buy_volume=pending, captured_at=time.time())
45
+
46
+ def trading_dates(self, market, first, last):
47
+ key = (market, first, last)
48
+ cached = self._calendar.get(key)
49
+ if cached and time.monotonic() - cached[0] < 300:
50
+ return cached[1]
51
+ values = self.market_request("xtdata.get_trading_dates", dict(market=market, start_time=first, end_time=last, count=-1))
52
+ if values is None:
53
+ raise RuntimeError("exchange calendar unavailable")
54
+ dates = set()
55
+ for value in values:
56
+ text = str(value).replace("-", "")
57
+ if len(text) == 8 and text.isdigit():
58
+ dates.add(text)
59
+ else:
60
+ stamp = quote_time_ms(value)
61
+ if stamp is not None:
62
+ dates.add(dt.datetime.fromtimestamp(stamp / 1000, SHANGHAI).strftime("%Y%m%d"))
63
+ result = sorted(dates)
64
+ self._calendar[key] = (time.monotonic(), result)
65
+ return result
66
+
67
+ def session_end(self, code, now):
68
+ local = dt.datetime.fromtimestamp(now, SHANGHAI)
69
+ date = local.strftime("%Y%m%d")
70
+ if date not in self.trading_dates(code.rsplit(".", 1)[-1], date, date):
71
+ return None
72
+ for begin, end in (("09:30", "11:30"), ("13:00", "14:57")):
73
+ a = dt.datetime.combine(local.date(), dt.time.fromisoformat(begin), SHANGHAI).timestamp()
74
+ b = dt.datetime.combine(local.date(), dt.time.fromisoformat(end), SHANGHAI).timestamp()
75
+ if a <= now < b:
76
+ return b
77
+ return None
78
+
79
+ def holding_deadline(self, code, rule):
80
+ if "at" in rule:
81
+ return rule["at"]
82
+ opened = dt.datetime.fromtimestamp(rule["opened_at"], SHANGHAI)
83
+ count = rule["trading_days"]
84
+ # Query actual exchange days; never infer holidays from weekdays.
85
+ dates = self.trading_dates(code.rsplit(".", 1)[-1], (opened + dt.timedelta(days=1)).strftime("%Y%m%d"),
86
+ (opened + dt.timedelta(days=count * 3 + 30)).strftime("%Y%m%d"))
87
+ if len(dates) < count:
88
+ raise ValueError("exchange calendar does not yet cover the requested holding period")
89
+ day = dt.datetime.strptime(dates[count - 1], "%Y%m%d").date()
90
+ return dt.datetime.combine(day, dt.time.fromisoformat(rule["exit_time"]), SHANGHAI).timestamp()
@@ -0,0 +1,161 @@
1
+ """Pure long-only cash-equity sizing. No connection or order is created here."""
2
+ from decimal import Decimal, ROUND_FLOOR
3
+
4
+ from .algo import positive
5
+
6
+
7
+ def instrument_rules(stock_code, min_volume=None, volume_step=None, price_tick=0.01):
8
+ symbol, _, market = str(stock_code).upper().rpartition(".")
9
+ stock = (len(symbol) == 6 and symbol.isdigit() and
10
+ ((market == "SH" and symbol.startswith(("60", "68"))) or
11
+ (market == "SZ" and symbol.startswith(("00", "30"))) or
12
+ (market == "BJ" and symbol.startswith(("43", "83", "87", "88", "92")))))
13
+ if not stock:
14
+ raise ValueError("position sizing currently supports SH/SZ/BJ cash equities only")
15
+ star = market == "SH" and symbol.startswith("68")
16
+ minimum = positive(min_volume if min_volume is not None else (200 if star else 100), "min_volume", integer=True)
17
+ step = positive(volume_step if volume_step is not None else (1 if star or market == "BJ" else 100), "volume_step", integer=True)
18
+ if minimum % step:
19
+ raise ValueError("min_volume must be a multiple of volume_step")
20
+ return dict(min_volume=minimum, volume_step=step, price_tick=positive(price_tick, "price_tick"),
21
+ source="explicit" if min_volume is not None or volume_step is not None else "cash_equity_defaults")
22
+
23
+
24
+ def calculate_position(*, stock_code, entry_price, method="risk_percent", stop_price=None,
25
+ total_asset=None, available_cash=None, risk_amount=None, risk_percent=None,
26
+ cash_amount=None, allocation_percent=None, allocation_base="total_asset",
27
+ target_weight=None, current_volume=0, pending_buy_volume=0,
28
+ max_position_percent=None, max_volume=None, reserved_cash=0,
29
+ commission_rate=0.0003, min_commission=5, sell_tax_rate=0.0005,
30
+ transfer_fee_rate=0.00001, slippage_per_share=0,
31
+ take_profit_price=None, min_volume=None, volume_step=None, price_tick=0.01):
32
+ """Largest legal buy quantity satisfying cash, estimated stop risk and position caps.
33
+
34
+ Percentages are fractions (0.02 = 2%). Fees are configurable estimates,
35
+ including minimum commission on each side. Missing cash imposes no cash cap;
36
+ server-side plans always supply a fresh account snapshot.
37
+ """
38
+ rules = instrument_rules(stock_code, min_volume, volume_step, price_tick)
39
+ p = Decimal(str(positive(entry_price, "entry_price")))
40
+ tick = Decimal(str(rules["price_tick"]))
41
+ if p % tick:
42
+ raise ValueError("entry_price must align with price_tick")
43
+ stop = Decimal(str(positive(stop_price, "stop_price"))) if stop_price is not None else None
44
+ if stop is not None and (stop >= p or stop % tick):
45
+ raise ValueError("long stop_price must be below entry_price and align with price_tick")
46
+ methods = ("risk_amount", "risk_percent", "cash_amount", "allocation_percent", "target_weight")
47
+ if method not in methods:
48
+ raise ValueError("unknown sizing method: " + str(method))
49
+ def number(value, name, zero=True):
50
+ return Decimal(str(positive(value, name, allow_zero=zero)))
51
+ def fraction(value, name):
52
+ value = number(value, name)
53
+ if value > 1:
54
+ raise ValueError(name + " must be a fraction between 0 and 1")
55
+ return value
56
+ equity = number(total_asset, "total_asset") if total_asset is not None else None
57
+ cash = number(available_cash, "available_cash") if available_cash is not None else None
58
+ reserved = number(reserved_cash, "reserved_cash")
59
+ spendable = max(Decimal(0), cash - reserved) if cash is not None else None
60
+ current = positive(current_volume, "current_volume", integer=True, allow_zero=True)
61
+ pending = positive(pending_buy_volume, "pending_buy_volume", integer=True, allow_zero=True)
62
+ commission = number(commission_rate, "commission_rate")
63
+ minimum_fee = number(min_commission, "min_commission")
64
+ tax = number(sell_tax_rate, "sell_tax_rate")
65
+ transfer = number(transfer_fee_rate, "transfer_fee_rate")
66
+ slip = number(slippage_per_share, "slippage_per_share")
67
+ risk = budget = None
68
+ if method in ("risk_amount", "risk_percent"):
69
+ if stop is None:
70
+ raise ValueError("risk sizing requires stop_price")
71
+ if method == "risk_amount":
72
+ risk = number(risk_amount, "risk_amount", False)
73
+ else:
74
+ if equity is None or equity <= 0:
75
+ raise ValueError("risk_percent requires positive total_asset")
76
+ risk = equity * fraction(risk_percent, "risk_percent")
77
+ theoretical = risk / (p - stop)
78
+ else:
79
+ if method == "cash_amount":
80
+ budget = number(cash_amount, "cash_amount", False)
81
+ elif method == "allocation_percent":
82
+ if allocation_base not in ("total_asset", "available_cash"):
83
+ raise ValueError("allocation_base must be total_asset or available_cash")
84
+ base = equity if allocation_base == "total_asset" else spendable
85
+ if base is None:
86
+ raise ValueError("allocation base is required")
87
+ budget = base * fraction(allocation_percent, "allocation_percent")
88
+ else:
89
+ if equity is None or equity <= 0:
90
+ raise ValueError("target_weight requires positive total_asset")
91
+ budget = max(Decimal(0), equity * fraction(target_weight, "target_weight") - (current + pending) * p)
92
+ theoretical = budget / p
93
+ caps = {"sizing_budget": theoretical}
94
+ if spendable is not None:
95
+ caps["available_cash"] = spendable / p
96
+ if max_position_percent is not None:
97
+ if equity is None or equity <= 0:
98
+ raise ValueError("max_position_percent requires positive total_asset")
99
+ caps["position_cap"] = max(Decimal(0), equity * fraction(max_position_percent, "max_position_percent") / p - current - pending)
100
+ if max_volume is not None:
101
+ caps["max_volume"] = Decimal(positive(max_volume, "max_volume", integer=True, allow_zero=True))
102
+ def costs(q):
103
+ if not q:
104
+ return Decimal(0), Decimal(0), Decimal(0), Decimal(0)
105
+ buy = q * p
106
+ entry_fee = max(minimum_fee, buy * commission) + buy * transfer
107
+ exit_value = q * stop if stop is not None else Decimal(0)
108
+ exit_fee = max(minimum_fee, exit_value * commission) + exit_value * (tax + transfer) if stop is not None else Decimal(0)
109
+ loss = q * (p - stop + slip) + entry_fee + exit_fee if stop is not None else Decimal(0)
110
+ return buy + entry_fee, loss, entry_fee, exit_fee
111
+ def failures(q):
112
+ cash_used, loss, _, _ = costs(q)
113
+ reasons = [name for name, cap in caps.items() if q > cap]
114
+ if spendable is not None and cash_used > spendable:
115
+ reasons.append("available_cash")
116
+ if budget is not None and cash_used > budget:
117
+ reasons.append("sizing_budget")
118
+ if risk is not None and loss > risk:
119
+ reasons.append("stop_risk_budget")
120
+ return sorted(set(reasons))
121
+ step = rules["volume_step"]
122
+ low, high = 0, int((min(caps.values()) / step).to_integral_value(rounding=ROUND_FLOOR))
123
+ while low < high:
124
+ mid = (low + high + 1) // 2
125
+ if failures(mid * step):
126
+ high = mid - 1
127
+ else:
128
+ low = mid
129
+ quantity = low * step
130
+ if quantity < rules["min_volume"]:
131
+ quantity = 0
132
+ used, loss, entry_fee, exit_fee = costs(quantity)
133
+ reasons = failures(quantity + step if quantity else rules["min_volume"])
134
+ if not quantity:
135
+ reasons.append("below_minimum_order")
136
+ profit = ratio = None
137
+ if take_profit_price is not None:
138
+ tp = number(take_profit_price, "take_profit_price", False)
139
+ if tp <= p or tp % tick:
140
+ raise ValueError("take_profit_price must exceed entry_price and align with price_tick")
141
+ exit_tp_fee = max(minimum_fee, quantity * tp * commission) + quantity * tp * (tax + transfer) if quantity else Decimal(0)
142
+ profit = quantity * (tp - p - slip) - entry_fee - exit_tp_fee
143
+ ratio = profit / loss if loss else None
144
+ return dict(stock_code=stock_code, method=method, quantity=quantity, theoretical_quantity=float(theoretical),
145
+ entry_price=float(p), stop_price=float(stop) if stop is not None else None,
146
+ notional=float(quantity * p), cash_required=float(used),
147
+ risk_budget=float(risk) if risk is not None else None,
148
+ estimated_loss=float(loss) if stop is not None else None,
149
+ risk_percent=float(loss / equity) if stop is not None and equity else None,
150
+ position_percent=float((current + pending + quantity) * p / equity) if equity else None,
151
+ entry_fee=float(entry_fee), exit_fee=float(exit_fee), slippage_amount=float(quantity * slip),
152
+ estimated_profit=float(profit) if profit is not None else None,
153
+ reward_risk_ratio=float(ratio) if ratio is not None else None,
154
+ limiting_reasons=reasons, rules=rules,
155
+ assumptions=dict(total_asset=float(equity) if equity is not None else None,
156
+ available_cash=float(cash) if cash is not None else None,
157
+ reserved_cash=float(reserved), current_volume=current, pending_buy_volume=pending,
158
+ commission_rate=float(commission), min_commission=float(minimum_fee),
159
+ sell_tax_rate=float(tax), transfer_fee_rate=float(transfer),
160
+ slippage_per_share=float(slip)),
161
+ risk_note="Estimated loss at the assumed stop fill; gaps, liquidity and T+1 can increase actual loss.")
@@ -197,6 +197,8 @@ class CfquantQmtBridge(object):
197
197
  )
198
198
 
199
199
  def _requires_qmt_thread(self, action):
200
+ if str(action).startswith("qmt_inner."):
201
+ return True
200
202
  if action in {"xtdata.subscribe_quote", "xtdata.subscribe_whole_quote", "xtdata.unsubscribe_quote",
201
203
  "xtdata.subscribe_whole_quote_latency", "cfquant.shared_quote_status", "cfquant.quote_heartbeat",
202
204
  "xtdata.subscribe_l2thousand", "xtdata.subscribe_l2thousand_queue"}:
@@ -239,6 +241,9 @@ class CfquantQmtBridge(object):
239
241
  return pack_response(request_id, ok=False, error=e)
240
242
 
241
243
  def _dispatch(self, action, params, msg):
244
+ if action.startswith("qmt_inner."):
245
+ from .qmt_inner_runtime import dispatch_inner_api
246
+ return dispatch_inner_api(self, action, params, msg)
242
247
  if action in ("cfquant.shared_quote_status", "cfquant.quote_heartbeat", "xtdata.subscribe_whole_quote_latency"):
243
248
  return self._quote_dispatch(action, params, msg)
244
249
  if action == "xttrader.get_hkt_exchange_rate":