cfquant 0.2.62__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.
- {cfquant-0.2.62/cfquant.egg-info → cfquant-0.2.64}/PKG-INFO +5 -1
- cfquant-0.2.62/PKG-INFO → cfquant-0.2.64/README.md +4 -25
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/__init__.py +2 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/cftrader.py +66 -1
- cfquant-0.2.64/cfquant/conditional_orders.py +149 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/management.py +32 -0
- cfquant-0.2.64/cfquant/planning_broker.py +90 -0
- cfquant-0.2.64/cfquant/position_sizing.py +161 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/qmt_bridge.py +5 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/qmt_component_login.py +8 -6
- cfquant-0.2.64/cfquant/qmt_inner.py +308 -0
- cfquant-0.2.64/cfquant/qmt_inner_catalog.py +55 -0
- cfquant-0.2.64/cfquant/qmt_inner_runtime.py +186 -0
- cfquant-0.2.64/cfquant/qmt_native_files.py +145 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/qmt_native_worker.py +117 -12
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/qmt_strategy_agent.js +68 -16
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/qmt_strategy_deploy.py +33 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/qmt_strategy_recovery.py +115 -42
- cfquant-0.2.64/cfquant/smart_orders.py +512 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_cftrader_tutorial.py +13 -4
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_qmt_auto_login_integration.py +24 -1
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_qmt_component_login.py +21 -0
- cfquant-0.2.64/cfquant/tests/test_qmt_inner.py +334 -0
- cfquant-0.2.64/cfquant/tests/test_qmt_inner_reference.py +116 -0
- cfquant-0.2.64/cfquant/tests/test_qmt_inner_web.py +62 -0
- cfquant-0.2.64/cfquant/tests/test_qmt_native_files.py +135 -0
- cfquant-0.2.64/cfquant/tests/test_qmt_native_reuse.py +135 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_qmt_native_worker.py +7 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_qmt_strategy_capabilities.py +99 -10
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_qmt_strategy_deploy.py +70 -0
- cfquant-0.2.64/cfquant/tests/test_smart_orders.py +386 -0
- cfquant-0.2.64/cfquant/tests/test_smart_orders_api.py +134 -0
- cfquant-0.2.64/cfquant/tests/test_smart_orders_docs.py +65 -0
- cfquant-0.2.64/cfquant/tests/test_strategy_recovery_api.py +239 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_trade_strategy_callback.py +4 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tx_trade_bridge.py +3 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/version.py +2 -2
- cfquant-0.2.62/README.md → cfquant-0.2.64/cfquant.egg-info/PKG-INFO +29 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant.egg-info/SOURCES.txt +19 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant_web_server.py +103 -6
- {cfquant-0.2.62 → cfquant-0.2.64}/pyproject.toml +1 -1
- {cfquant-0.2.62 → cfquant-0.2.64}/qmt_scripts/CFQUANT.py +27 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +27 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/qmt_scripts/CFQUANT_LITE.py +270 -1
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → cfquant-0.2.64}/web_dashboard/api-tester.js +1 -1
- {cfquant-0.2.62 → cfquant-0.2.64}/web_dashboard/app.js +40 -16
- {cfquant-0.2.62 → cfquant-0.2.64}/web_dashboard/docs.js +2 -2
- {cfquant-0.2.62 → cfquant-0.2.64}/web_dashboard/index.html +16 -6
- {cfquant-0.2.62 → cfquant-0.2.64}/web_dashboard/python-api.js +25 -9
- cfquant-0.2.64/web_dashboard/qmt-inner-api-data.js +4211 -0
- cfquant-0.2.64/web_dashboard/smart-order-api-data.js +94 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/web_dashboard/styles.css +11 -1
- {cfquant-0.2.62 → cfquant-0.2.64}/LICENSE +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/LTtx/__init__.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/LTtx/tx/Config.txt +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/LTtx/tx/LTtx_server.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/LTtx/tx/__init__.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/LTtx/tx/tx.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/MANIFEST.in +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/_editable_install.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/account_routing.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/algo.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/algo_broker.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/batch_orders.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/build_info.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/callback_dispatcher.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/channels.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/cli.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/client.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/config.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/diagnostics.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/flow_control.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/hub.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/level2.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/log_management.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/logging_i18n.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/normal_bridge.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/order_identity.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/order_meta.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/pipe_bridge.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/pipe_client.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/pipe_hub.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/pipe_transport.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/protocol.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/qmt_binding_defaults.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/qmt_binding_settings.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/qmt_component_agent.js +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/qmt_credentials.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/qmt_dependencies.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/qmt_pipe_runtime.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/qmt_strategy_package.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/qmt_strategy_runtime.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/qmt_update.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/quote_history.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/quote_leases.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/quote_service.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/remote_adapter.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/remote_management.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/remote_supervisor.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/rpc_timing.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/runtime_report.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/shared_quotes.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/stock_connect.py +0 -0
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/29_Level2/350/241/214/346/203/205/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/6_/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/_helpers.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/readme.md +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_account_query_timing.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_account_subscription_probe.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_advanced_parity.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_algo.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_algo_broker.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_api_tester_browser.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_api_tester_web.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_binding_live_refresh_browser.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_binding_runtime_browser.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_callback_recovery.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_callback_requests.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_cftrader.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_cftrader_web.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_credit_compat.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_dataframe_encoding.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_json_serialization.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_latency_flow_control.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_lite_account_routing.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_lite_deployment_paths.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_lite_parity.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_lite_qmt_dispatch.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_local_trading_calendar.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_log_management.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_management.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_management_batch.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_market_data_fallback.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_market_timing.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_order_identity.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_order_time_callback.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_pipe_shutdown.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_project_updater.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_python_api_reference.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_qmt_auto_login_browser.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_qmt_binding_defaults.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_qmt_binding_settings.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_qmt_dependencies.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_qmt_fileless.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_qmt_strategy_browser.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_qmt_strategy_recovery.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_qmt_strategy_web.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_qmt_update.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_quote_latency.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_quote_latency_encoding.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_quote_service.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_remote_management.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_remote_management_browser.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_remote_version_check.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_shared_quotes.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_shared_quotes_generated.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_startup_recovery.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_stock_connect.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_stock_connect_browser.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_subscription_congestion.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_table_conversion.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_trader_subscription_recovery.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_trading_dates.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_trading_monitor.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_trading_monitor_browser.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_tutorial_reader.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_tx_auto_import.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tests/test_tx_shutdown.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/trading_calendar.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/trading_monitor.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/tx.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/xtconstant.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/xtdata.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/xttrader.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant/xttype.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant.egg-info/dependency_links.txt +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant.egg-info/entry_points.txt +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant.egg-info/requires.txt +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant.egg-info/top_level.txt +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/cfquant_pipe_hub.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/qmt_scripts/__init__.py +0 -0
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → 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
- {cfquant-0.2.62 → cfquant-0.2.64}/requirements.txt +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/setup.cfg +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/web_dashboard/__init__.py +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/web_dashboard/avatars/amber-pulse.svg +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/web_dashboard/avatars/copper-grid.svg +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/web_dashboard/avatars/market-blue.svg +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/web_dashboard/avatars/rose-circuit.svg +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/web_dashboard/avatars/signal-green.svg +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/web_dashboard/avatars/slate-wave.svg +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/web_dashboard/avatars/teal-orbit.svg +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/web_dashboard/avatars/violet-node.svg +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/web_dashboard/cftrader-api-data.js +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/web_dashboard/favicon.svg +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/web_dashboard/python-api-data.js +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/web_dashboard/remote-management.js +0 -0
- {cfquant-0.2.62 → cfquant-0.2.64}/web_dashboard/trading-monitor.js +0 -0
- {cfquant-0.2.62 → 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.
|
|
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 使用自包含入口;低延迟场景可拆分普通桥和交易桥。 |
|
|
@@ -283,6 +284,8 @@ print(result["status"], result["message"])
|
|
|
283
284
|
|
|
284
285
|
`version_info(include_remote=False)` 只查询本地版本;`updates.check(force=True)` 只检查更新。按需更新也识别同版本号的安装包变化,首次安装包哈希未知时会执行一次更新以建立记录。真正需要更新时,默认要求先关闭相关 QMT;也可显式传入 `auto_close_qmt=True` 允许自动关闭。`restart=False` 可留待调用方重启。部分 QMT 部署失败返回 `update_incomplete`,已安装但尚未重启返回 `restart_required`;执行更新失败会抛出 `ManagementError`。
|
|
285
286
|
|
|
287
|
+
外部程序复用绑定时的 `runtime` 对象,直接调用 `runtime.recover_strategy("YOUR_ACCOUNT_ID", account_type="STOCK")` 即可检测并尝试恢复已停止的 QMT 托管策略。本地托管实例由 SDK 自动读取管理凭据。也支持 `runtime.accounts.recover_strategy(...)` 和鉴权后的 `POST /api/qmt/strategies/recover`。返回值区分已提交启动、模型运行和通信确认,具体条件及示例见 [策略恢复接口](docs/第三方项目管理SDK.md#外部程序恢复-qmt-托管策略)。
|
|
288
|
+
|
|
286
289
|
模拟/实盘参数需与实际账号一致。完整的启动策略、更新接口、重复绑定行为与错误处理见 [第三方项目管理 SDK](docs/第三方项目管理SDK.md)。
|
|
287
290
|
|
|
288
291
|
## 从 `xtquant` 迁移到 `cfquant`
|
|
@@ -395,6 +398,7 @@ restart_cfquant.bat 重启
|
|
|
395
398
|
| QMT 综合部署教程 | [QMT 部署教程](docs/QMT部署教程.md) |
|
|
396
399
|
| cftrader 批量同步/异步下单与撤单 | [cftrader 批量交易与撤单](docs/cftrader批量交易.md),含 100 单本地基准 |
|
|
397
400
|
| cftrader TWAP / VWAP / POV | [算法交易与共享全推行情](docs/algorithm_trading.md),含自动订阅、生命周期和接口示例 |
|
|
401
|
+
| 条件单 / 仓位计算 / 下单计划 | [条件单、仓位计算与下单计划](docs/conditional_orders.md),含止盈止损、到时平仓、回撤 / 保本保护、风险仓位及 Python / HTTP 接口 |
|
|
398
402
|
| 通用模式部署 | [通用模式部署指南](docs/通用模式部署指南.md) |
|
|
399
403
|
| 极致模式部署 | [极致模式部署指南](docs/极致模式部署指南.md) |
|
|
400
404
|
| 高级模式部署 | [高级模式部署指南](docs/高级模式部署指南.md) |
|
|
@@ -1,28 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: cfquant
|
|
3
|
-
Version: 0.2.62
|
|
4
|
-
Summary: QMT local bridge and xtquant-compatible Python client.
|
|
5
|
-
License: MIT
|
|
6
|
-
Requires-Python: >=3.8
|
|
7
|
-
Description-Content-Type: text/markdown
|
|
8
|
-
License-File: LICENSE
|
|
9
|
-
Requires-Dist: pandas>=1.0
|
|
10
|
-
Requires-Dist: packaging>=20
|
|
11
|
-
Requires-Dist: psutil>=5.8
|
|
12
|
-
Requires-Dist: tomli>=1.1.0; python_version < "3.11"
|
|
13
|
-
Requires-Dist: pycryptodome>=3.20
|
|
14
|
-
Requires-Dist: pytz
|
|
15
|
-
Requires-Dist: tabulate
|
|
16
|
-
Requires-Dist: pandas-market-calendars
|
|
17
|
-
Requires-Dist: importlib-resources>=5
|
|
18
|
-
Requires-Dist: frida==17.16.4; sys_platform == "win32"
|
|
19
|
-
Requires-Dist: pywin32>=306; sys_platform == "win32"
|
|
20
|
-
Provides-Extra: dev
|
|
21
|
-
Requires-Dist: build>=1.0; extra == "dev"
|
|
22
|
-
Requires-Dist: twine>=5.0; extra == "dev"
|
|
23
|
-
Provides-Extra: zmq
|
|
24
|
-
Requires-Dist: pyzmq>=25; extra == "zmq"
|
|
25
|
-
|
|
26
1
|
# cfquant
|
|
27
2
|
|
|
28
3
|
<p>
|
|
@@ -48,6 +23,7 @@ Requires-Dist: pyzmq>=25; extra == "zmq"
|
|
|
48
23
|
|---|---|
|
|
49
24
|
| MiniQMT / `xtquant` 兼容调用 | 原有外部 Python 策略可以沿用接近 `xtdata`、`XtQuantTrader`、`StockAccount` 的写法,降低迁移成本。 |
|
|
50
25
|
| 大 QMT 本地桥接 | 把大 QMT 内部的行情、查询、交易、撤单和回调能力桥接给外部 Python 程序和 Web 控制台。 |
|
|
26
|
+
| 大 QMT 原生接口类 | 独立的 `QmtInnerApi` 面向外部数据查询、研究和实盘交易,按系统、行情、交易、回调、扩展数据与因子细分,保留原生函数名与参数,详见 [内置接口说明](docs/大QMT内置接口.md)。 |
|
|
51
27
|
| Web 控制台 | 在浏览器里完成账号绑定、QMT 入口脚本指引、在线检测、接口调试、版本更新和回滚。 |
|
|
52
28
|
| 多账号 / 多 QMT 路由 | 按 `bridge_id`、`account_type`、`account_id` 路由请求,适配多资金账号、普通账户、信用账户和多 QMT 终端。 |
|
|
53
29
|
| 通用、极致、高级三种模式 | 普通环境快速跑通;受限 QMT 使用自包含入口;低延迟场景可拆分普通桥和交易桥。 |
|
|
@@ -283,6 +259,8 @@ print(result["status"], result["message"])
|
|
|
283
259
|
|
|
284
260
|
`version_info(include_remote=False)` 只查询本地版本;`updates.check(force=True)` 只检查更新。按需更新也识别同版本号的安装包变化,首次安装包哈希未知时会执行一次更新以建立记录。真正需要更新时,默认要求先关闭相关 QMT;也可显式传入 `auto_close_qmt=True` 允许自动关闭。`restart=False` 可留待调用方重启。部分 QMT 部署失败返回 `update_incomplete`,已安装但尚未重启返回 `restart_required`;执行更新失败会抛出 `ManagementError`。
|
|
285
261
|
|
|
262
|
+
外部程序复用绑定时的 `runtime` 对象,直接调用 `runtime.recover_strategy("YOUR_ACCOUNT_ID", account_type="STOCK")` 即可检测并尝试恢复已停止的 QMT 托管策略。本地托管实例由 SDK 自动读取管理凭据。也支持 `runtime.accounts.recover_strategy(...)` 和鉴权后的 `POST /api/qmt/strategies/recover`。返回值区分已提交启动、模型运行和通信确认,具体条件及示例见 [策略恢复接口](docs/第三方项目管理SDK.md#外部程序恢复-qmt-托管策略)。
|
|
263
|
+
|
|
286
264
|
模拟/实盘参数需与实际账号一致。完整的启动策略、更新接口、重复绑定行为与错误处理见 [第三方项目管理 SDK](docs/第三方项目管理SDK.md)。
|
|
287
265
|
|
|
288
266
|
## 从 `xtquant` 迁移到 `cfquant`
|
|
@@ -395,6 +373,7 @@ restart_cfquant.bat 重启
|
|
|
395
373
|
| QMT 综合部署教程 | [QMT 部署教程](docs/QMT部署教程.md) |
|
|
396
374
|
| cftrader 批量同步/异步下单与撤单 | [cftrader 批量交易与撤单](docs/cftrader批量交易.md),含 100 单本地基准 |
|
|
397
375
|
| cftrader TWAP / VWAP / POV | [算法交易与共享全推行情](docs/algorithm_trading.md),含自动订阅、生命周期和接口示例 |
|
|
376
|
+
| 条件单 / 仓位计算 / 下单计划 | [条件单、仓位计算与下单计划](docs/conditional_orders.md),含止盈止损、到时平仓、回撤 / 保本保护、风险仓位及 Python / HTTP 接口 |
|
|
398
377
|
| 通用模式部署 | [通用模式部署指南](docs/通用模式部署指南.md) |
|
|
399
378
|
| 极致模式部署 | [极致模式部署指南](docs/极致模式部署指南.md) |
|
|
400
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
|
|
@@ -314,6 +314,17 @@ class RuntimeManager:
|
|
|
314
314
|
"""Initialize the first binding; subsequent calls upsert that binding."""
|
|
315
315
|
return self.accounts.upsert(account_id, qmt_dir, _initialize=True, **options)
|
|
316
316
|
|
|
317
|
+
def recover_strategy(self, account_id=None, *, account_type=None, bridge_id=None,
|
|
318
|
+
account_key=None, timeout=180):
|
|
319
|
+
"""Recover a binding using the same managed service as initialize().
|
|
320
|
+
|
|
321
|
+
Reuse this RuntimeManager after binding accounts. Local managed services
|
|
322
|
+
automatically use home/management.key, just like account management.
|
|
323
|
+
"""
|
|
324
|
+
return self.accounts.recover_strategy(account_id, account_type=account_type,
|
|
325
|
+
bridge_id=bridge_id, account_key=account_key,
|
|
326
|
+
timeout=timeout)
|
|
327
|
+
|
|
317
328
|
def wait_ready(self, account_id, *, account_type="STOCK", bridge_id=None, timeout=120):
|
|
318
329
|
deadline = time.monotonic() + timeout
|
|
319
330
|
last = None
|
|
@@ -363,6 +374,27 @@ class AccountManager:
|
|
|
363
374
|
def status(self, timeout=None):
|
|
364
375
|
return self.runtime._request("GET", "/api/bindings/status", timeout=timeout)
|
|
365
376
|
|
|
377
|
+
def recover_strategy(self, account_id=None, *, account_type=None, bridge_id=None,
|
|
378
|
+
account_key=None, timeout=180):
|
|
379
|
+
"""Check and recover this binding's stopped managed QMT strategies.
|
|
380
|
+
|
|
381
|
+
Requires enabled strategy automation and a completed deployment. Returns
|
|
382
|
+
running/start_requested and per-target recovery states; submitting a start
|
|
383
|
+
is not confirmation that the strategy or its bridge is already running.
|
|
384
|
+
Uses the Web service, so it also works while the QMT bridge is offline.
|
|
385
|
+
"""
|
|
386
|
+
body = {key: str(value).strip() for key, value in {
|
|
387
|
+
"account_id": account_id, "account_type": account_type,
|
|
388
|
+
"bridge_id": bridge_id, "account_key": account_key,
|
|
389
|
+
}.items() if value is not None}
|
|
390
|
+
if not body.get("account_id") and not body.get("account_key"):
|
|
391
|
+
raise ValueError("account_id or account_key is required")
|
|
392
|
+
if any(not value for value in body.values()):
|
|
393
|
+
raise ValueError("account selectors must not be empty")
|
|
394
|
+
if not self.runtime.attached:
|
|
395
|
+
self.runtime.status() # Verify managed instance ownership first.
|
|
396
|
+
return self.runtime._request("POST", "/api/qmt/strategies/recover", body, timeout=timeout)
|
|
397
|
+
|
|
366
398
|
def upsert(self, account_id, qmt_dir, *, account_type="STOCK", mode="ctypes",
|
|
367
399
|
auto_start_qmt=False, deploy_strategy=True, strategy_autorun=True,
|
|
368
400
|
live=True, auto_close_qmt=False, force=False, _initialize=False, **options):
|
|
@@ -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.")
|