cfquant 0.2.55__tar.gz → 0.2.56__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.55 → cfquant-0.2.56}/PKG-INFO +3 -1
- {cfquant-0.2.55 → cfquant-0.2.56}/README.md +5 -3
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/batch_orders.py +15 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/cftrader.py +21 -3
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/client.py +13 -3
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/normal_bridge.py +24 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/pipe_client.py +20 -13
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/protocol.py +5 -4
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/qmt_dependencies.py +33 -12
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/qmt_pipe_runtime.py +28 -5
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/qmt_strategy_deploy.py +66 -35
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/rpc_timing.py +15 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/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 +150 -0
- cfquant-0.2.56/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 +170 -0
- cfquant-0.2.56/cfquant/tests/test_account_query_timing.py +131 -0
- cfquant-0.2.56/cfquant/tests/test_binding_live_refresh_browser.py +95 -0
- cfquant-0.2.56/cfquant/tests/test_callback_recovery.py +287 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_cftrader.py +17 -1
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_cftrader_tutorial.py +5 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_market_timing.py +1 -1
- cfquant-0.2.56/cfquant/tests/test_qmt_dependencies.py +107 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_qmt_fileless.py +129 -3
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_qmt_strategy_browser.py +14 -6
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_qmt_strategy_deploy.py +29 -0
- cfquant-0.2.56/cfquant/tests/test_remote_version_check.py +72 -0
- cfquant-0.2.56/cfquant/tests/test_trader_subscription_recovery.py +345 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_tutorial_reader.py +3 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tx_trade_bridge.py +36 -12
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/version.py +2 -2
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/xttrader.py +201 -43
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant.egg-info/PKG-INFO +3 -1
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant.egg-info/SOURCES.txt +5 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant_web_server.py +463 -228
- {cfquant-0.2.55 → cfquant-0.2.56}/pyproject.toml +1 -1
- {cfquant-0.2.55 → cfquant-0.2.56}/qmt_scripts/CFQUANT_LITE.py +102 -15
- {cfquant-0.2.55 → cfquant-0.2.56}/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 +102 -15
- {cfquant-0.2.55 → cfquant-0.2.56}/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 +102 -15
- {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/app.js +199 -52
- {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/index.html +16 -5
- {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/styles.css +35 -6
- cfquant-0.2.55/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 -88
- cfquant-0.2.55/cfquant/tests/test_qmt_dependencies.py +0 -55
- {cfquant-0.2.55 → cfquant-0.2.56}/LICENSE +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/LTtx/__init__.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/LTtx/tx/Config.txt +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/LTtx/tx/LTtx_server.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/LTtx/tx/__init__.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/LTtx/tx/tx.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/MANIFEST.in +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/__init__.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/_editable_install.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/account_routing.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/build_info.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/callback_dispatcher.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/channels.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/cli.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/config.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/diagnostics.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/flow_control.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/hub.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/level2.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/log_management.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/logging_i18n.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/management.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/order_identity.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/order_meta.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/pipe_bridge.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/pipe_hub.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/pipe_transport.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/qmt_binding_settings.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/qmt_bridge.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/qmt_component_agent.js +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/qmt_component_login.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/qmt_credentials.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/qmt_strategy_package.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/qmt_strategy_runtime.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/quote_history.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/runtime_report.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/stock_connect.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/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.55 → cfquant-0.2.56}/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.55 → cfquant-0.2.56}/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.55 → cfquant-0.2.56}/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.55 → cfquant-0.2.56}/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.55 → cfquant-0.2.56}/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.55 → cfquant-0.2.56}/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.55 → cfquant-0.2.56}/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.55 → cfquant-0.2.56}/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.55 → cfquant-0.2.56}/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.55 → cfquant-0.2.56}/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.55 → cfquant-0.2.56}/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.55 → cfquant-0.2.56}/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.55 → cfquant-0.2.56}/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.55 → cfquant-0.2.56}/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.55 → cfquant-0.2.56}/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.55 → cfquant-0.2.56}/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.55 → cfquant-0.2.56}/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.55 → cfquant-0.2.56}/cfquant/tests/29_Level2/350/241/214/346/203/205/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/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.55 → cfquant-0.2.56}/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.55 → cfquant-0.2.56}/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.55 → cfquant-0.2.56}/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.55 → cfquant-0.2.56}/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.55 → cfquant-0.2.56}/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.55 → cfquant-0.2.56}/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.55 → cfquant-0.2.56}/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.55 → cfquant-0.2.56}/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.55 → cfquant-0.2.56}/cfquant/tests/6_/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/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.55 → cfquant-0.2.56}/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.55 → cfquant-0.2.56}/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.55 → cfquant-0.2.56}/cfquant/tests/_helpers.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/log/cftrader/346/250/241/346/213/237/350/264/246/345/217/267/346/211/271/351/207/217/344/270/213/345/215/225/346/265/213/350/257/225_20260911_024617.md" +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/log/cftrader/346/250/241/346/213/237/350/264/246/345/217/267/346/211/271/351/207/217/344/270/213/345/215/225/346/265/213/350/257/225_20260921_175732.md" +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/readme.md +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_advanced_parity.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_api_tester_browser.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_api_tester_web.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_binding_runtime_browser.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_callback_requests.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_cftrader_web.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_credit_compat.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_dataframe_encoding.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_json_serialization.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_latency_flow_control.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_lite_account_routing.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_lite_deployment_paths.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_lite_parity.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_lite_qmt_dispatch.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_local_trading_calendar.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_log_management.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_management.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_management_batch.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_market_data_fallback.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_order_identity.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_order_time_callback.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_project_updater.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_python_api_reference.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_qmt_auto_login_browser.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_qmt_auto_login_integration.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_qmt_binding_settings.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_qmt_component_login.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_qmt_strategy_web.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_quote_latency.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_quote_latency_encoding.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_startup_recovery.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_stock_connect.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_stock_connect_browser.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_table_conversion.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_trade_strategy_callback.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_trading_dates.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_tx_auto_import.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tests/test_tx_shutdown.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/trading_calendar.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/tx.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/xtconstant.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/xtdata.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant/xttype.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant.egg-info/dependency_links.txt +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant.egg-info/entry_points.txt +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant.egg-info/requires.txt +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant.egg-info/top_level.txt +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/cfquant_pipe_hub.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/qmt_scripts/CFQUANT.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/qmt_scripts/__init__.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_CTYPE_ALL_LOWLAT_SH.py" +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_CTYPE_ALL_LOWLAT_SZ.py" +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/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.55 → cfquant-0.2.56}/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.55 → cfquant-0.2.56}/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.55 → cfquant-0.2.56}/requirements.txt +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/setup.cfg +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/__init__.py +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/api-tester.js +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/avatars/amber-pulse.svg +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/avatars/copper-grid.svg +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/avatars/market-blue.svg +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/avatars/rose-circuit.svg +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/avatars/signal-green.svg +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/avatars/slate-wave.svg +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/avatars/teal-orbit.svg +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/avatars/violet-node.svg +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/cftrader-api-data.js +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/docs.js +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/favicon.svg +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/python-api-data.js +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/web_dashboard/python-api.js +0 -0
- {cfquant-0.2.55 → cfquant-0.2.56}/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.56
|
|
4
4
|
Summary: QMT local bridge and xtquant-compatible Python client.
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.8
|
|
@@ -343,6 +343,8 @@ trader.disconnect()
|
|
|
343
343
|
|
|
344
344
|
`XtQuantTrader`、`StockAccount`、常用行情查询和交易方法保持接近 `xtquant` 的调用方式,原有回调类通常可以继续使用。仍有部分 QMT 专有接口、字段和返回结构存在兼容边界,使用前请查看 [xtquant 原版接口适配清单](docs/xtquant原版接口适配清单.md) 和 [QMT 函数封装能力清单](docs/QMT函数封装能力清单.md)。
|
|
345
345
|
|
|
346
|
+
账号订阅成功后,SDK 会在后台约每两秒重新确认订阅。用户程序保持运行时,QMT、cfquant Web 或通信服务重启后,待桥接策略恢复授权、通道可用,SDK 会自动恢复该账号的新回调;默认 Web 路由和直接 Pipe/LTtx 连接均适用。主动 `unsubscribe(account)` 后不会再恢复该账号,`stop()` / `disconnect()` 会停止后台恢复。此机制只恢复账号订阅,不补发停机期间的回调,也不重试委托或撤单。升级 SDK 后需先重启一次用户程序加载新代码,此后无需随 QMT 重启用户程序。
|
|
347
|
+
|
|
346
348
|
推荐迁移顺序:**行情查询 → 资金/持仓 → 回调 → 模拟委托 → 模拟撤单 → 正式策略**。每一步都先在 Web 控制台接口测试和日志中确认,再进入下一步。
|
|
347
349
|
|
|
348
350
|
## Web 控制台
|
|
@@ -317,9 +317,11 @@ trader.disconnect()
|
|
|
317
317
|
|
|
318
318
|
### 回调和接口差异
|
|
319
319
|
|
|
320
|
-
`XtQuantTrader`、`StockAccount`、常用行情查询和交易方法保持接近 `xtquant` 的调用方式,原有回调类通常可以继续使用。仍有部分 QMT 专有接口、字段和返回结构存在兼容边界,使用前请查看 [xtquant 原版接口适配清单](docs/xtquant原版接口适配清单.md) 和 [QMT 函数封装能力清单](docs/QMT函数封装能力清单.md)。
|
|
321
|
-
|
|
322
|
-
|
|
320
|
+
`XtQuantTrader`、`StockAccount`、常用行情查询和交易方法保持接近 `xtquant` 的调用方式,原有回调类通常可以继续使用。仍有部分 QMT 专有接口、字段和返回结构存在兼容边界,使用前请查看 [xtquant 原版接口适配清单](docs/xtquant原版接口适配清单.md) 和 [QMT 函数封装能力清单](docs/QMT函数封装能力清单.md)。
|
|
321
|
+
|
|
322
|
+
账号订阅成功后,SDK 会在后台约每两秒重新确认订阅。用户程序保持运行时,QMT、cfquant Web 或通信服务重启后,待桥接策略恢复授权、通道可用,SDK 会自动恢复该账号的新回调;默认 Web 路由和直接 Pipe/LTtx 连接均适用。主动 `unsubscribe(account)` 后不会再恢复该账号,`stop()` / `disconnect()` 会停止后台恢复。此机制只恢复账号订阅,不补发停机期间的回调,也不重试委托或撤单。升级 SDK 后需先重启一次用户程序加载新代码,此后无需随 QMT 重启用户程序。
|
|
323
|
+
|
|
324
|
+
推荐迁移顺序:**行情查询 → 资金/持仓 → 回调 → 模拟委托 → 模拟撤单 → 正式策略**。每一步都先在 Web 控制台接口测试和日志中确认,再进入下一步。
|
|
323
325
|
|
|
324
326
|
## Web 控制台
|
|
325
327
|
|
|
@@ -197,6 +197,8 @@ def batch_cancel_result_rows(cancels, seqs=None):
|
|
|
197
197
|
def batch_result(account, batch_id, asynchronous, results, operation="order"):
|
|
198
198
|
counts = {name: sum(row["status"] == name for row in results)
|
|
199
199
|
for name in ("submitted", "failed", "unknown", "skipped")}
|
|
200
|
+
counts["rejected"] = sum(row.get("dispatched") is False for row in results)
|
|
201
|
+
counts["retryable"] = sum(row.get("safe_to_retry") is True for row in results)
|
|
200
202
|
return dict(counts, batch_id=batch_id, account=account, asynchronous=asynchronous,
|
|
201
203
|
operation=operation, execution="qmt", total=len(results), attempted=len(results) - counts["skipped"],
|
|
202
204
|
ok=counts["submitted"] == len(results), results=results)
|
|
@@ -207,6 +209,19 @@ def batch_unknown(rows, error):
|
|
|
207
209
|
row.update(status="unknown", ok=None, error=str(error) or type(error).__name__)
|
|
208
210
|
|
|
209
211
|
|
|
212
|
+
def batch_rejected(rows, error):
|
|
213
|
+
"""Mark a rejection known to happen before QMT dispatch."""
|
|
214
|
+
details = getattr(error, "error_details", None)
|
|
215
|
+
details = details if isinstance(details, dict) else {}
|
|
216
|
+
message = str(error) or type(error).__name__
|
|
217
|
+
for row in rows:
|
|
218
|
+
row.update(status="failed", ok=False, error=message, dispatched=False,
|
|
219
|
+
safe_to_retry=details.get("safe_to_retry") is True,
|
|
220
|
+
retry_when=details.get("retry_when", ""),
|
|
221
|
+
error_code=details.get("code", ""),
|
|
222
|
+
error_stage=details.get("stage", "before_dispatch"))
|
|
223
|
+
|
|
224
|
+
|
|
210
225
|
def validate_batch_response(result, params, asynchronous):
|
|
211
226
|
expected = params["orders"]
|
|
212
227
|
if (not isinstance(result, dict) or result.get("execution") != "qmt"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Order-only extensions sharing an existing XtQuantTrader session and callbacks."""
|
|
2
2
|
|
|
3
3
|
from .batch_orders import (
|
|
4
|
-
batch_cancel_result_rows, batch_result, batch_result_rows, batch_unknown,
|
|
4
|
+
batch_cancel_result_rows, batch_rejected, batch_result, batch_result_rows, batch_unknown,
|
|
5
5
|
prepare_batch_cancels, prepare_batch_orders, validate_batch_cancel_response, validate_batch_response,
|
|
6
6
|
)
|
|
7
7
|
from .protocol import new_id
|
|
@@ -86,9 +86,18 @@ class CfQuantTrader:
|
|
|
86
86
|
result = validate_batch_response(response, params, asynchronous)
|
|
87
87
|
except Exception as error:
|
|
88
88
|
results = batch_result_rows(rows, seqs)
|
|
89
|
-
|
|
89
|
+
if getattr(error, "dispatched", None) is False:
|
|
90
|
+
batch_rejected(results, error)
|
|
91
|
+
else:
|
|
92
|
+
batch_unknown(results, "Batch response unavailable; QMT may still execute the batch: %s" % error)
|
|
90
93
|
result = batch_result(account, batch_id, asynchronous, results)
|
|
91
94
|
result["request_error"] = str(error) or type(error).__name__
|
|
95
|
+
details = getattr(error, "error_details", None)
|
|
96
|
+
if isinstance(details, dict):
|
|
97
|
+
result.update(error_code=details.get("code", ""), error_stage=details.get("stage", ""),
|
|
98
|
+
dispatched=details.get("dispatched"), safe_to_retry=details.get("safe_to_retry") is True,
|
|
99
|
+
retry_when=details.get("retry_when", ""),
|
|
100
|
+
runtime_instance_id=details.get("runtime_instance_id", ""))
|
|
92
101
|
if asynchronous:
|
|
93
102
|
for row in result["results"]:
|
|
94
103
|
if row["status"] in ("failed", "skipped"):
|
|
@@ -113,7 +122,16 @@ class CfQuantTrader:
|
|
|
113
122
|
return validate_batch_cancel_response(response, params, asynchronous)
|
|
114
123
|
except Exception as error:
|
|
115
124
|
results = batch_cancel_result_rows(rows, seqs)
|
|
116
|
-
|
|
125
|
+
if getattr(error, "dispatched", None) is False:
|
|
126
|
+
batch_rejected(results, error)
|
|
127
|
+
else:
|
|
128
|
+
batch_unknown(results, "Batch cancel response unavailable; QMT may still execute the batch: %s" % error)
|
|
117
129
|
result = batch_result(account, batch_id, asynchronous, results, operation="cancel")
|
|
118
130
|
result["request_error"] = str(error) or type(error).__name__
|
|
131
|
+
details = getattr(error, "error_details", None)
|
|
132
|
+
if isinstance(details, dict):
|
|
133
|
+
result.update(error_code=details.get("code", ""), error_stage=details.get("stage", ""),
|
|
134
|
+
dispatched=details.get("dispatched"), safe_to_retry=details.get("safe_to_retry") is True,
|
|
135
|
+
retry_when=details.get("retry_when", ""),
|
|
136
|
+
runtime_instance_id=details.get("runtime_instance_id", ""))
|
|
119
137
|
return result
|
|
@@ -12,8 +12,17 @@ from .protocol import decode_value, dumps_message, loads_message, new_id, pack_r
|
|
|
12
12
|
from .rpc_timing import RpcTiming
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
class CfquantError(RuntimeError):
|
|
16
|
-
|
|
15
|
+
class CfquantError(RuntimeError):
|
|
16
|
+
"""RPC failure retaining the server's structured rejection details."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, message="", details=None):
|
|
19
|
+
super(CfquantError, self).__init__(message)
|
|
20
|
+
self.error_details = dict(details) if isinstance(details, dict) else {}
|
|
21
|
+
self.code = self.error_details.get("code", "")
|
|
22
|
+
self.stage = self.error_details.get("stage", "")
|
|
23
|
+
self.dispatched = self.error_details.get("dispatched")
|
|
24
|
+
self.safe_to_retry = self.error_details.get("safe_to_retry", False) is True
|
|
25
|
+
self.retry_when = self.error_details.get("retry_when", "")
|
|
17
26
|
|
|
18
27
|
|
|
19
28
|
class CfquantTimeout(TimeoutError):
|
|
@@ -209,6 +218,7 @@ class LTtxRpcClient(object):
|
|
|
209
218
|
with self._pending_lock:
|
|
210
219
|
self._pending[request_id] = q
|
|
211
220
|
self._push("request", raw, request_channel or self.request_channel)
|
|
221
|
+
timing.sent()
|
|
212
222
|
try:
|
|
213
223
|
msg = _wait_response(q, effective_timeout)
|
|
214
224
|
except queue.Empty:
|
|
@@ -216,7 +226,7 @@ class LTtxRpcClient(object):
|
|
|
216
226
|
timing.received(msg)
|
|
217
227
|
if not msg.get("ok"):
|
|
218
228
|
err = msg.get("error") or {}
|
|
219
|
-
raise CfquantError(err.get("message") or str(err))
|
|
229
|
+
raise CfquantError(err.get("message") or str(err), details=err)
|
|
220
230
|
return timing.decode(msg.get("result"), decode_value)
|
|
221
231
|
except (KeyboardInterrupt, SystemExit):
|
|
222
232
|
self.close()
|
|
@@ -837,6 +837,7 @@ class NormalQmtBridge(TxTradeBridge):
|
|
|
837
837
|
if not active:
|
|
838
838
|
return
|
|
839
839
|
msg = active[0][0]
|
|
840
|
+
dispatch_started = time.perf_counter()
|
|
840
841
|
result = self._dispatch(msg.get("action"), msg.get("params") or {}, msg)
|
|
841
842
|
with self.coalesce_lock:
|
|
842
843
|
entry = self.coalesced_requests.pop(coalesce_key, None)
|
|
@@ -847,6 +848,15 @@ class NormalQmtBridge(TxTradeBridge):
|
|
|
847
848
|
encoded_result = encode_response_result(result)
|
|
848
849
|
while response_waiters:
|
|
849
850
|
waiter_msg, _ = response_waiters[0]
|
|
851
|
+
if msg.get("_response_meta") and str(msg.get("action", "")).startswith("xttrader.query_stock_"):
|
|
852
|
+
# Coalesced readers share execution, but each keeps its own ID
|
|
853
|
+
# and queue delay (including readers arriving during execution).
|
|
854
|
+
timing = dict(msg["_response_meta"]["timing"]["qmt"])
|
|
855
|
+
received = waiter_msg.get("_received_monotonic")
|
|
856
|
+
if received is not None:
|
|
857
|
+
timing["queue_ms"] = round(max(0.0, dispatch_started - received) * 1000, 3)
|
|
858
|
+
timing["coalesced_request_id"] = msg.get("id")
|
|
859
|
+
waiter_msg["_response_meta"] = {"timing": {"qmt": timing}}
|
|
850
860
|
self._send_response(waiter_msg, result, encoded_result=encoded_result)
|
|
851
861
|
response_waiters.pop(0)
|
|
852
862
|
self._log(
|
|
@@ -864,6 +874,13 @@ class NormalQmtBridge(TxTradeBridge):
|
|
|
864
874
|
% (source, msg.get("action"), msg.get("id"), len(waiters), e)
|
|
865
875
|
)
|
|
866
876
|
for waiter_msg, _ in waiters:
|
|
877
|
+
if msg.get("_response_meta") and str(msg.get("action", "")).startswith("xttrader.query_stock_"):
|
|
878
|
+
timing = dict(msg["_response_meta"]["timing"]["qmt"])
|
|
879
|
+
received = waiter_msg.get("_received_monotonic")
|
|
880
|
+
if received is not None:
|
|
881
|
+
timing["queue_ms"] = round(max(0.0, dispatch_started - received) * 1000, 3)
|
|
882
|
+
timing["coalesced_request_id"] = msg.get("id")
|
|
883
|
+
waiter_msg["_response_meta"] = {"timing": {"qmt": timing}}
|
|
867
884
|
self._send_error(waiter_msg, e)
|
|
868
885
|
|
|
869
886
|
def _on_whole_quote(self, data):
|
|
@@ -908,6 +925,13 @@ class NormalQmtBridge(TxTradeBridge):
|
|
|
908
925
|
client_id = msg.get("client_id") or msg.get("reply_channel")
|
|
909
926
|
if not client_id:
|
|
910
927
|
return
|
|
928
|
+
if (not msg.get("_response_meta") and str(msg.get("action", "")).startswith("xttrader.query_stock_")
|
|
929
|
+
and str(error) == "read-only request expired before QMT execution"):
|
|
930
|
+
received = msg.get("_received_monotonic")
|
|
931
|
+
msg["_response_meta"] = {"timing": {"qmt": {
|
|
932
|
+
"available": True, "executed": False, "native_ms": 0.0, "execute_ms": 0.0,
|
|
933
|
+
"queue_ms": round((time.perf_counter() - received) * 1000, 3) if received is not None else None,
|
|
934
|
+
}}}
|
|
911
935
|
self._log(
|
|
912
936
|
"normal bridge send_error action=%s id=%s client_id=%s error=%s"
|
|
913
937
|
% (msg.get("action"), msg.get("id"), client_id, error)
|
|
@@ -48,18 +48,24 @@ class PipeRpcClient(object):
|
|
|
48
48
|
if self._started:
|
|
49
49
|
self._started = False
|
|
50
50
|
self._close_conns_locked()
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
51
|
+
try:
|
|
52
|
+
self._rx_conn = connect_pipe(self.pipe_name, timeout_ms=self.connect_timeout_ms)
|
|
53
|
+
self._rx_conn.write_frame(dumps_pipe_message({
|
|
54
|
+
"type": "hello",
|
|
55
|
+
"role": "api_rx",
|
|
56
|
+
"client_id": self.client_id,
|
|
57
|
+
}))
|
|
58
|
+
self._tx_conn = connect_pipe(self.pipe_name, timeout_ms=self.connect_timeout_ms)
|
|
59
|
+
self._tx_conn.write_frame(dumps_pipe_message({
|
|
60
|
+
"type": "hello",
|
|
61
|
+
"role": "api_tx",
|
|
62
|
+
"client_id": self.client_id,
|
|
63
|
+
}))
|
|
64
|
+
except BaseException:
|
|
65
|
+
# A retry must not orphan the receive pipe when opening or
|
|
66
|
+
# registering the send pipe failed halfway through startup.
|
|
67
|
+
self.close()
|
|
68
|
+
raise
|
|
63
69
|
self._started = True
|
|
64
70
|
self._closed = False
|
|
65
71
|
generation = self._event_dispatcher.start()
|
|
@@ -95,6 +101,7 @@ class PipeRpcClient(object):
|
|
|
95
101
|
self._pending[request_id] = q
|
|
96
102
|
try:
|
|
97
103
|
self._send_request(raw, request_channel or self.request_channel)
|
|
104
|
+
timing.sent()
|
|
98
105
|
except Exception:
|
|
99
106
|
self.close()
|
|
100
107
|
raise
|
|
@@ -111,7 +118,7 @@ class PipeRpcClient(object):
|
|
|
111
118
|
err = msg.get("error") or {}
|
|
112
119
|
from .client import CfquantError
|
|
113
120
|
|
|
114
|
-
raise CfquantError(err.get("message") or str(err))
|
|
121
|
+
raise CfquantError(err.get("message") or str(err), details=err)
|
|
115
122
|
return timing.decode(msg.get("result"), decode_value)
|
|
116
123
|
except (KeyboardInterrupt, SystemExit):
|
|
117
124
|
self.close()
|
|
@@ -215,10 +215,11 @@ def encode_error(error):
|
|
|
215
215
|
return None
|
|
216
216
|
if isinstance(error, dict):
|
|
217
217
|
return error
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
218
|
+
details = getattr(error, "error_details", None)
|
|
219
|
+
result = dict(details) if isinstance(details, dict) else {}
|
|
220
|
+
result.setdefault("type", type(error).__name__)
|
|
221
|
+
result["message"] = str(error)
|
|
222
|
+
return result
|
|
222
223
|
|
|
223
224
|
|
|
224
225
|
def encode_value(value):
|
|
@@ -13,7 +13,7 @@ LOGIN_MODULES = ("frida", "win32api", "win32crypt", "win32event", "win32gui", "w
|
|
|
13
13
|
_LOCK = threading.Lock()
|
|
14
14
|
_CACHE = {}
|
|
15
15
|
_PROBE_CODE = r'''
|
|
16
|
-
import importlib, importlib.metadata, json, pathlib, sys
|
|
16
|
+
import importlib, importlib.metadata, json, os, pathlib, sys
|
|
17
17
|
checks = []
|
|
18
18
|
for name in MODULES:
|
|
19
19
|
try:
|
|
@@ -28,10 +28,14 @@ for name in ('pywin32', 'frida'):
|
|
|
28
28
|
except importlib.metadata.PackageNotFoundError:
|
|
29
29
|
packages[name] = ''
|
|
30
30
|
prefix = pathlib.Path(sys.prefix)
|
|
31
|
+
base_prefix = pathlib.Path(getattr(sys, 'base_prefix', sys.prefix))
|
|
31
32
|
conda = (prefix / 'conda-meta').is_dir()
|
|
33
|
+
venv = not conda and prefix != base_prefix
|
|
34
|
+
environment = 'conda' if conda else 'venv' if venv else 'python'
|
|
32
35
|
print(json.dumps(dict(python_executable=sys.executable, python_version=sys.version.split()[0],
|
|
33
|
-
prefix=str(prefix),
|
|
36
|
+
prefix=str(prefix), base_prefix=str(base_prefix), environment=environment,
|
|
34
37
|
global_python=sys.prefix == sys.base_prefix,
|
|
38
|
+
conda_executable=os.environ.get('CONDA_EXE', '') if conda else '',
|
|
35
39
|
conda_pywin32=conda and bool(list((prefix / 'conda-meta').glob('pywin32-*.json'))),
|
|
36
40
|
supported=sys.platform == 'win32', checks=checks, packages=packages), ensure_ascii=True))
|
|
37
41
|
'''
|
|
@@ -55,23 +59,40 @@ def _finish_report(report):
|
|
|
55
59
|
report["message"] = "QMT 自动登录组件不可用(%s)。可取消自动启动,保存配置后手动启动并登录 QMT。" % names
|
|
56
60
|
commands = []
|
|
57
61
|
python = "& " + _ps_quote(report["python_executable"])
|
|
62
|
+
environment = str(report.get("environment") or "").lower()
|
|
63
|
+
if environment not in {"conda", "venv", "python"}:
|
|
64
|
+
environment = "python" if report.get("global_python") else "venv"
|
|
58
65
|
if report.get("supported") and failed:
|
|
59
66
|
if any(item["name"].startswith("win32") for item in failed):
|
|
60
|
-
if report.get("conda_pywin32"):
|
|
61
|
-
|
|
67
|
+
if environment == "conda" and report.get("conda_pywin32"):
|
|
68
|
+
conda = report.get("conda_executable") or "conda"
|
|
69
|
+
commands.append("& %s install --prefix %s --force-reinstall 'pywin32>=306'" % (
|
|
70
|
+
_ps_quote(conda), _ps_quote(report["prefix"])))
|
|
62
71
|
else:
|
|
63
|
-
commands.append(python + " -m pip install --no-cache-dir --force-reinstall 'pywin32>=306'")
|
|
64
|
-
if
|
|
72
|
+
commands.append(python + " -m pip install --no-cache-dir --upgrade --force-reinstall 'pywin32>=306'")
|
|
73
|
+
if environment == "python":
|
|
65
74
|
commands.append(python + " -m pywin32_postinstall -install")
|
|
66
75
|
if any(item["name"] == "frida" for item in failed):
|
|
67
|
-
commands.append(python + " -m pip install --no-cache-dir --force-reinstall 'frida>=17.0,<18'")
|
|
76
|
+
commands.append(python + " -m pip install --no-cache-dir --upgrade --force-reinstall 'frida>=17.0,<18'")
|
|
68
77
|
commands.append(python + " -c " + _ps_quote("import " + ", ".join(LOGIN_MODULES) + "; print('QMT login components OK')"))
|
|
69
78
|
report["repair_commands"] = commands
|
|
70
|
-
|
|
71
|
-
"
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
79
|
+
if environment == "conda":
|
|
80
|
+
report["repair_note"] = (
|
|
81
|
+
"已检测到 Conda 环境。先停止 CFQuant,在已激活该环境的 Anaconda Prompt 或 PowerShell 中执行;"
|
|
82
|
+
"命令会绑定当前环境的前缀。验证通过后重启 CFQuant,再重新检测。"
|
|
83
|
+
)
|
|
84
|
+
elif environment == "venv":
|
|
85
|
+
report["repair_note"] = (
|
|
86
|
+
"已检测到 venv 虚拟环境。先停止 CFQuant,在普通 PowerShell 中执行下面的命令;"
|
|
87
|
+
"命令已绑定当前 venv 的 Python,不要切换到系统 Python,也不要运行 pywin32_postinstall。"
|
|
88
|
+
"验证通过后重启 CFQuant,再重新检测。"
|
|
89
|
+
)
|
|
90
|
+
else:
|
|
91
|
+
report["repair_note"] = (
|
|
92
|
+
"已检测到独立 Python 环境。先停止 CFQuant,在 PowerShell 中执行下面的命令;"
|
|
93
|
+
"命令已绑定当前 Python,安装完成后会执行 pywin32_postinstall。"
|
|
94
|
+
"验证通过后重启 CFQuant,再重新检测。"
|
|
95
|
+
)
|
|
75
96
|
return report
|
|
76
97
|
|
|
77
98
|
|
|
@@ -5,11 +5,23 @@ import os as _cq_os
|
|
|
5
5
|
import sys as _cq_sys
|
|
6
6
|
import threading as _cq_threading
|
|
7
7
|
import time as _cq_time
|
|
8
|
+
import uuid as _cq_uuid
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ManagedStrategyError(RuntimeError):
|
|
12
|
+
"""A rejection made before entering any QMT business dispatcher."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, message, code, runtime_instance_id, retry_when=""):
|
|
15
|
+
super(ManagedStrategyError, self).__init__(message)
|
|
16
|
+
self.error_details = dict(code=code, stage="before_dispatch", dispatched=False,
|
|
17
|
+
safe_to_retry=bool(retry_when), retry_when=retry_when,
|
|
18
|
+
runtime_instance_id=runtime_instance_id)
|
|
8
19
|
|
|
9
20
|
|
|
10
21
|
class ManagedPipeRuntime(object):
|
|
11
22
|
def __init__(self, descriptor=None):
|
|
12
23
|
self.descriptor = dict(descriptor or {})
|
|
24
|
+
self.runtime_instance_id = _cq_uuid.uuid4().hex
|
|
13
25
|
self.active = False
|
|
14
26
|
self.approved = False
|
|
15
27
|
self.revoked = False
|
|
@@ -49,7 +61,8 @@ class ManagedPipeRuntime(object):
|
|
|
49
61
|
state = "error" if self.failure else ("stopped" if not self.active or self.revoked
|
|
50
62
|
else ("running" if ready and self.approved else "loaded"))
|
|
51
63
|
return dict(self.descriptor, state=state, error=self.failure,
|
|
52
|
-
pid=_cq_os.getpid(), updated_at=_cq_time.time()
|
|
64
|
+
pid=_cq_os.getpid(), updated_at=_cq_time.time(),
|
|
65
|
+
runtime_instance_id=self.runtime_instance_id)
|
|
53
66
|
|
|
54
67
|
def control(self, desired):
|
|
55
68
|
if desired.get("runtime_key") != self.descriptor.get("runtime_key"):
|
|
@@ -78,9 +91,18 @@ class ManagedPipeRuntime(object):
|
|
|
78
91
|
return self.control(params or {})
|
|
79
92
|
if action == "cfquant.status":
|
|
80
93
|
return function(action, params, msg)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
94
|
+
with self.mutex:
|
|
95
|
+
if not self.active or self.revoked:
|
|
96
|
+
raise ManagedStrategyError("The account's cfquant strategy mode has changed.",
|
|
97
|
+
"STRATEGY_MODE_CHANGED", self.runtime_instance_id)
|
|
98
|
+
if not self.approved:
|
|
99
|
+
raise ManagedStrategyError(
|
|
100
|
+
"Managed strategy is awaiting account mode confirmation from cfquant Web.",
|
|
101
|
+
"ACCOUNT_MODE_UNCONFIRMED", self.runtime_instance_id, "account_ready")
|
|
102
|
+
result = dispatch_request(action, params, msg)
|
|
103
|
+
if action == "cfquant.ping" and isinstance(result, dict):
|
|
104
|
+
result = dict(result, managed_runtime=self.status())
|
|
105
|
+
return result
|
|
84
106
|
return guarded
|
|
85
107
|
|
|
86
108
|
def wrap(self, function, reject=False):
|
|
@@ -89,7 +111,8 @@ class ManagedPipeRuntime(object):
|
|
|
89
111
|
with self.mutex:
|
|
90
112
|
if not self.active or self.revoked:
|
|
91
113
|
if reject:
|
|
92
|
-
raise
|
|
114
|
+
raise ManagedStrategyError("The account's cfquant strategy mode has changed.",
|
|
115
|
+
"STRATEGY_MODE_CHANGED", self.runtime_instance_id)
|
|
93
116
|
return None
|
|
94
117
|
self.calls += 1
|
|
95
118
|
try:
|
|
@@ -5,7 +5,8 @@ import copy
|
|
|
5
5
|
import hashlib
|
|
6
6
|
import io
|
|
7
7
|
import json
|
|
8
|
-
import
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
import re
|
|
11
12
|
import threading
|
|
@@ -15,9 +16,12 @@ import traceback
|
|
|
15
16
|
import uuid
|
|
16
17
|
from xml.dom import minidom
|
|
17
18
|
|
|
18
|
-
from .qmt_strategy_package import build_package
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
from .qmt_strategy_package import build_package
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
21
25
|
SCRIPTS = {"ctypes": "CFQUANT_CTYPE_ALL_LOWLAT.py", "lite": "CFQUANT_LITE.py",
|
|
22
26
|
"lttx": "CFQUANT.py"}
|
|
23
27
|
MODE_ALIASES = {
|
|
@@ -577,9 +581,12 @@ class QmtStrategyManager:
|
|
|
577
581
|
with self.lock:
|
|
578
582
|
jobs = [job for job in self.jobs.values() if job["account_key"] == account_key]
|
|
579
583
|
fields = ("root", "state", "message", "error", "mode", "updated_at", "backup")
|
|
580
|
-
result = {"enabled": any(job.get("enabled") for job in jobs),
|
|
581
|
-
"targets": [dict({k: job.get(k, "") for k in fields},
|
|
582
|
-
strategies=[r["name"] for r in job.get("roles", [])]
|
|
584
|
+
result = {"enabled": any(job.get("enabled") for job in jobs),
|
|
585
|
+
"targets": [dict({k: job.get(k, "") for k in fields},
|
|
586
|
+
strategies=[r["name"] for r in job.get("roles", [])],
|
|
587
|
+
control_sync=[dict(r.get("control_sync") or {}, role=r["role"],
|
|
588
|
+
strategy=r["name"])
|
|
589
|
+
for r in job.get("roles", [])]) for job in jobs]}
|
|
583
590
|
if account_key in self.errors:
|
|
584
591
|
detail = str(self.errors[account_key] or "策略部署失败")
|
|
585
592
|
result.update(error=detail, message="账号已保存,策略部署失败: " + detail)
|
|
@@ -737,6 +744,8 @@ class QmtStrategyManager:
|
|
|
737
744
|
def _sync_pipe_runtime(self, job):
|
|
738
745
|
if self.runtime_request is None:
|
|
739
746
|
return
|
|
747
|
+
from .channels import channels_for_bridge
|
|
748
|
+
|
|
740
749
|
desired = self._desired(job)
|
|
741
750
|
for role in job.get("retired_roles", []):
|
|
742
751
|
try:
|
|
@@ -744,16 +753,34 @@ class QmtStrategyManager:
|
|
|
744
753
|
except Exception:
|
|
745
754
|
pass
|
|
746
755
|
for role in job["roles"]:
|
|
756
|
+
identity = role["identity"]
|
|
757
|
+
sync = role.setdefault("control_sync", {})
|
|
758
|
+
previous_error = sync.get("last_error", "")
|
|
759
|
+
sync.update(last_attempt_at=time.time(), pipe_name=identity.get("pipe_name"),
|
|
760
|
+
request_channel=((identity.get("channels") or {}).get("normal")
|
|
761
|
+
or identity.get("normal_channel")
|
|
762
|
+
or channels_for_bridge(identity.get("bridge_id"))["normal"]))
|
|
747
763
|
try:
|
|
748
|
-
report = self.runtime_request(
|
|
749
|
-
if (isinstance(report, dict)
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
764
|
+
report = self.runtime_request(identity, desired)
|
|
765
|
+
if (not isinstance(report, dict) or report.get("runtime_key") != desired["runtime_key"]
|
|
766
|
+
or report.get("role") != role["role"]):
|
|
767
|
+
raise ValueError("Managed strategy control response scope mismatch")
|
|
768
|
+
# Success records an acknowledged control, not necessarily a
|
|
769
|
+
# ready bridge: QMT may still be binding its trading context.
|
|
770
|
+
sync.update(last_success_at=time.time(), state=report.get("state"),
|
|
771
|
+
pid=report.get("pid"), generation=report.get("generation"), last_error="")
|
|
772
|
+
# Only the external manager writes status files.
|
|
773
|
+
_write_json(role["runtime_status_path"], report)
|
|
774
|
+
except Exception as error:
|
|
754
775
|
# A stopped/not-yet-imported strategy has no Pipe endpoint.
|
|
755
|
-
#
|
|
756
|
-
|
|
776
|
+
# Retry next cycle, but retain diagnostics instead of hiding
|
|
777
|
+
# why a restarted strategy is still awaiting confirmation.
|
|
778
|
+
detail = "%s: %s" % (type(error).__name__, str(error))
|
|
779
|
+
sync["last_error"] = detail
|
|
780
|
+
if detail != previous_error:
|
|
781
|
+
logger.warning("QMT strategy control sync failed account=%s role=%s pipe=%s channel=%s: %s",
|
|
782
|
+
job["account_key"], role["role"], sync["pipe_name"],
|
|
783
|
+
sync["request_channel"], detail)
|
|
757
784
|
|
|
758
785
|
@staticmethod
|
|
759
786
|
def _role_endpoint(role):
|
|
@@ -790,23 +817,18 @@ class QmtStrategyManager:
|
|
|
790
817
|
with self.lock:
|
|
791
818
|
changed = False
|
|
792
819
|
for job in self.jobs.values():
|
|
793
|
-
before = copy.deepcopy(job)
|
|
794
|
-
if job.get("state") == "disabled":
|
|
795
|
-
continue
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
#
|
|
799
|
-
#
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
if
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
retry_count = int(job.get("deploy_retry_count") or 0)
|
|
806
|
-
if retry_count >= 5 or time.time() < float(job.get("deploy_retry_after") or 0):
|
|
807
|
-
continue
|
|
808
|
-
try:
|
|
809
|
-
self._advance(job)
|
|
820
|
+
before = copy.deepcopy(job)
|
|
821
|
+
if job.get("state") == "disabled":
|
|
822
|
+
continue
|
|
823
|
+
try:
|
|
824
|
+
transition = job.get("enabled") and self._prepare_generation_transition(job)
|
|
825
|
+
# A QMT restart loses its in-memory approval. Keep sending
|
|
826
|
+
# the selected mode even if offline deployment failed,
|
|
827
|
+
# its retries are exhausted, or its files are unavailable.
|
|
828
|
+
self._sync_pipe_runtime(job)
|
|
829
|
+
if transition:
|
|
830
|
+
self._control(job)
|
|
831
|
+
self._advance(job)
|
|
810
832
|
except Exception as error:
|
|
811
833
|
retry_count = int(job.get("deploy_retry_count") or 0) + 1
|
|
812
834
|
job["deploy_retry_count"] = retry_count
|
|
@@ -826,12 +848,21 @@ class QmtStrategyManager:
|
|
|
826
848
|
if changed:
|
|
827
849
|
self._save()
|
|
828
850
|
|
|
829
|
-
def _advance(self, job):
|
|
830
|
-
root = qmt_root(job["root"]
|
|
851
|
+
def _advance(self, job):
|
|
852
|
+
root = qmt_root(job["root"])
|
|
831
853
|
if self.process_checker(root):
|
|
832
|
-
self._sync_pipe_runtime(job)
|
|
833
854
|
self._observe_runtime(job)
|
|
834
855
|
return
|
|
856
|
+
# Retry limits apply to offline deployment, never to mode confirmation
|
|
857
|
+
# or observing a strategy that has come back online.
|
|
858
|
+
if job.get("state") == "error":
|
|
859
|
+
retry_count = int(job.get("deploy_retry_count") or 0)
|
|
860
|
+
if (not retry_count and not job.get("control_generation")
|
|
861
|
+
and not _generation_values(job.get("accepted_generations"))):
|
|
862
|
+
return
|
|
863
|
+
if retry_count >= 5 or time.time() < float(job.get("deploy_retry_after") or 0):
|
|
864
|
+
return
|
|
865
|
+
root = qmt_root(root, validate=True)
|
|
835
866
|
job["retired_roles"] = []
|
|
836
867
|
if (job.get("state") in ("configured", "running", "waiting_start", "waiting_manual_start")
|
|
837
868
|
and all(role.get("imported") for role in job["roles"])):
|
|
@@ -7,23 +7,36 @@ class RpcTiming(object):
|
|
|
7
7
|
self.output = output
|
|
8
8
|
self.started = time.perf_counter()
|
|
9
9
|
self.received_at = None
|
|
10
|
+
self.sent_at = None
|
|
10
11
|
if output is not None:
|
|
11
12
|
output.update({
|
|
12
13
|
"request_id": request_id,
|
|
14
|
+
"started_at": time.time(),
|
|
13
15
|
"qmt": {"available": False, "reason": "no_response"},
|
|
14
16
|
"rpc": {"response_received": False},
|
|
15
17
|
})
|
|
16
18
|
|
|
19
|
+
def sent(self):
|
|
20
|
+
self.sent_at = time.perf_counter()
|
|
21
|
+
if self.output is not None:
|
|
22
|
+
self.output["rpc"].update({
|
|
23
|
+
"sent_at": time.time(),
|
|
24
|
+
"connect_send_ms": round((self.sent_at - self.started) * 1000, 3),
|
|
25
|
+
})
|
|
26
|
+
|
|
17
27
|
def received(self, message):
|
|
18
28
|
self.received_at = time.perf_counter()
|
|
19
29
|
if self.output is None:
|
|
20
30
|
return
|
|
21
31
|
self.output["rpc"].update({
|
|
32
|
+
"received_at": time.time(),
|
|
22
33
|
"response_received": True,
|
|
23
34
|
"response_ok": bool(message.get("ok")),
|
|
24
35
|
"wait_ms": round((self.received_at - self.started) * 1000, 3),
|
|
25
36
|
"json_decode_ms": message.get("_client_parse_ms"),
|
|
26
37
|
})
|
|
38
|
+
if self.sent_at is not None:
|
|
39
|
+
self.output["rpc"]["response_wait_ms"] = round((self.received_at - self.sent_at) * 1000, 3)
|
|
27
40
|
meta = message.get("meta") or {}
|
|
28
41
|
qmt = (meta.get("timing") or {}).get("qmt")
|
|
29
42
|
self.output["qmt"] = dict(qmt) if isinstance(qmt, dict) else {
|
|
@@ -44,3 +57,5 @@ class RpcTiming(object):
|
|
|
44
57
|
self.output["rpc"]["total_ms"] = elapsed
|
|
45
58
|
if self.received_at is None:
|
|
46
59
|
self.output["rpc"]["wait_ms"] = elapsed
|
|
60
|
+
if self.sent_at is not None:
|
|
61
|
+
self.output["rpc"]["response_wait_ms"] = round((time.perf_counter() - self.sent_at) * 1000, 3)
|