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.
- {cfquant-0.2.63 → cfquant-0.2.64}/PKG-INFO +3 -1
- {cfquant-0.2.63 → cfquant-0.2.64}/README.md +2 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/__init__.py +2 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/cftrader.py +66 -1
- cfquant-0.2.64/cfquant/conditional_orders.py +149 -0
- cfquant-0.2.64/cfquant/planning_broker.py +90 -0
- cfquant-0.2.64/cfquant/position_sizing.py +161 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/qmt_bridge.py +5 -0
- 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.63 → cfquant-0.2.64}/cfquant/qmt_strategy_agent.js +62 -13
- cfquant-0.2.64/cfquant/smart_orders.py +512 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_cftrader_tutorial.py +13 -4
- 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.63 → cfquant-0.2.64}/cfquant/tests/test_qmt_strategy_capabilities.py +76 -7
- 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.63 → cfquant-0.2.64}/cfquant/tests/test_trade_strategy_callback.py +4 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tx_trade_bridge.py +3 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/version.py +2 -2
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant.egg-info/PKG-INFO +3 -1
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant.egg-info/SOURCES.txt +15 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant_web_server.py +61 -3
- {cfquant-0.2.63 → cfquant-0.2.64}/pyproject.toml +1 -1
- {cfquant-0.2.63 → cfquant-0.2.64}/qmt_scripts/CFQUANT.py +27 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +27 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/qmt_scripts/CFQUANT_LITE.py +270 -1
- {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
- {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
- {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
- {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
- {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/api-tester.js +1 -1
- {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/app.js +40 -16
- {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/docs.js +2 -2
- {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/index.html +16 -6
- {cfquant-0.2.63 → 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.63 → cfquant-0.2.64}/web_dashboard/styles.css +11 -1
- {cfquant-0.2.63 → cfquant-0.2.64}/LICENSE +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/LTtx/__init__.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/LTtx/tx/Config.txt +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/LTtx/tx/LTtx_server.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/LTtx/tx/__init__.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/LTtx/tx/tx.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/MANIFEST.in +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/_editable_install.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/account_routing.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/algo.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/algo_broker.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/batch_orders.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/build_info.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/callback_dispatcher.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/channels.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/cli.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/client.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/config.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/diagnostics.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/flow_control.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/hub.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/level2.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/log_management.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/logging_i18n.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/management.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/normal_bridge.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/order_identity.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/order_meta.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/pipe_bridge.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/pipe_client.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/pipe_hub.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/pipe_transport.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/protocol.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/qmt_binding_defaults.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/qmt_binding_settings.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/qmt_component_agent.js +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/qmt_component_login.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/qmt_credentials.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/qmt_dependencies.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/qmt_native_files.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/qmt_native_worker.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/qmt_pipe_runtime.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/qmt_strategy_deploy.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/qmt_strategy_package.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/qmt_strategy_recovery.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/qmt_strategy_runtime.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/qmt_update.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/quote_history.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/quote_leases.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/quote_service.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/remote_adapter.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/remote_management.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/remote_supervisor.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/rpc_timing.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/runtime_report.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/shared_quotes.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/stock_connect.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/_helpers.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/readme.md +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_account_query_timing.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_account_subscription_probe.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_advanced_parity.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_algo.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_algo_broker.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_api_tester_browser.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_api_tester_web.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_binding_live_refresh_browser.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_binding_runtime_browser.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_callback_recovery.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_callback_requests.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_cftrader.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_cftrader_web.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_credit_compat.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_dataframe_encoding.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_json_serialization.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_latency_flow_control.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_lite_account_routing.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_lite_deployment_paths.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_lite_parity.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_lite_qmt_dispatch.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_local_trading_calendar.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_log_management.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_management.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_management_batch.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_market_data_fallback.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_market_timing.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_order_identity.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_order_time_callback.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_pipe_shutdown.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_project_updater.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_python_api_reference.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_qmt_auto_login_browser.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_qmt_auto_login_integration.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_qmt_binding_defaults.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_qmt_binding_settings.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_qmt_component_login.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_qmt_dependencies.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_qmt_fileless.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_qmt_native_files.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_qmt_native_reuse.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_qmt_native_worker.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_qmt_strategy_browser.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_qmt_strategy_deploy.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_qmt_strategy_recovery.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_qmt_strategy_web.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_qmt_update.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_quote_latency.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_quote_latency_encoding.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_quote_service.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_remote_management.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_remote_management_browser.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_remote_version_check.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_shared_quotes.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_shared_quotes_generated.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_startup_recovery.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_stock_connect.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_stock_connect_browser.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_strategy_recovery_api.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_subscription_congestion.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_table_conversion.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_trader_subscription_recovery.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_trading_dates.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_trading_monitor.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_trading_monitor_browser.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_tutorial_reader.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_tx_auto_import.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tests/test_tx_shutdown.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/trading_calendar.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/trading_monitor.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/tx.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/xtconstant.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/xtdata.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/xttrader.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant/xttype.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant.egg-info/dependency_links.txt +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant.egg-info/entry_points.txt +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant.egg-info/requires.txt +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant.egg-info/top_level.txt +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/cfquant_pipe_hub.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/qmt_scripts/__init__.py +0 -0
- {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
- {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
- {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
- {cfquant-0.2.63 → cfquant-0.2.64}/requirements.txt +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/setup.cfg +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/__init__.py +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/avatars/amber-pulse.svg +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/avatars/copper-grid.svg +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/avatars/market-blue.svg +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/avatars/rose-circuit.svg +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/avatars/signal-green.svg +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/avatars/slate-wave.svg +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/avatars/teal-orbit.svg +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/avatars/violet-node.svg +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/cftrader-api-data.js +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/favicon.svg +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/python-api-data.js +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/remote-management.js +0 -0
- {cfquant-0.2.63 → cfquant-0.2.64}/web_dashboard/trading-monitor.js +0 -0
- {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.
|
|
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":
|